Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/editor/src/assets/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@
"export-tooltip": "Export (Strg + E)",
"exporting": "Exportieren",

"annotation-time": "Annotationszeit",
"show-delta": "Delta anzeigen",
"confirm-task-annotation-tooltip": "Bestätigen",
"skip-task-annotation-tooltip": "Überspringen",

Expand Down
2 changes: 2 additions & 0 deletions apps/editor/src/assets/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@
"export-tooltip": "Export (Ctrl + E)",
"exporting": "Exporting",

"annotation-time": "Annotation Time",
"show-delta": "Show delta",
"confirm-task-annotation-tooltip": "Confirm",
"skip-task-annotation-tooltip": "Skip",

Expand Down
2 changes: 1 addition & 1 deletion apps/editor/src/components/editor/ai-bar/ai-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AnnotationStatus,
BlueButtonParam,
color,
fontSize,
Expand All @@ -20,7 +21,6 @@ import styled from "styled-components";

import { useStore } from "../../../app/root-store";
import { whoHome } from "../../../constants";
import { AnnotationStatus } from "../../../models/who/annotation";
import { AnnotationData } from "../../../models/who/annotationData";

const AIBarSheet = styled(Sheet)`
Expand Down
8 changes: 6 additions & 2 deletions apps/editor/src/components/editor/layers/layers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import {
stopPropagation,
styledScrollbarMixin,
SubtleText,
TaskType,
useDelay,
useDoubleTap,
useForwardEvent,
useModalRoot,
useShortTap,
useTranslation,
} from "@visian/ui-shared";
import { Pixel } from "@visian/utils";
import { isFromWHO, Pixel } from "@visian/utils";
import { Observer, observer } from "mobx-react-lite";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
Expand Down Expand Up @@ -365,6 +366,8 @@ export const Layers: React.FC = observer(() => {
const layerCount = layers?.length;
const activeLayer = store?.editor.activeDocument?.activeLayer;
const activeLayerIndex = layers?.findIndex((layer) => layer === activeLayer);
const isSupervisorMode =
isFromWHO() && store?.currentTask?.kind === TaskType.Review;
return (
<>
<FloatingUIButton
Expand All @@ -389,7 +392,8 @@ export const Layers: React.FC = observer(() => {
tooltipTx="add-annotation-layer"
isDisabled={
!layerCount ||
layerCount >= (store?.editor.activeDocument?.maxLayers || 0)
layerCount >= (store?.editor.activeDocument?.maxLayers || 0) ||
isSupervisorMode
}
onPointerDown={
store?.editor.activeDocument?.addNewAnnotationLayer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./supervisor-panel";
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
BooleanParam,
color,
Divider,
fontSize,
Icon,
Modal,
TaskType,
Text,
UserRole,
} from "@visian/ui-shared";
import { observer } from "mobx-react-lite";
import React, { useState } from "react";
import styled from "styled-components";
import { useStore } from "../../../app/root-store";

interface AnnotatorSectionProps {
annotatorRole: UserRole;
annotatorName: string;
annotationTime?: string;
colorAddition: string;
colorDeletion?: string;
isLast?: boolean;
}

const SectionContainer = styled.div<{ isLast?: boolean }>`
display: flex;
flex-direction: column;
margin-bottom: ${(props) => (props.isLast ? "0px" : "12px")};
width: 100%;
`;

const AnnotatorInformationContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`;

const AnnotationTimeContainer = styled.div`
display: flex;
flex-direction: column;
margin-top: 8px;
`;

const TypeText = styled(Text)`
color: ${color("lightText")};
font-size: ${fontSize("small")};
`;

const InformationText = styled(Text)`
font-size: 18px;
`;

const EditCircle = styled.div<{ circleColor: string }>`
width: 17px;
height: 17px;
background-color: ${(props) =>
color(props.circleColor as any) || props.circleColor};
border-radius: 50%;
`;
const CircleContainer = styled.div`
display: flex;
flex-direction: row;
min-width: 40px;
justify-content: space-between;
`;

const AnnotatorSection: React.FC<AnnotatorSectionProps> = ({
annotatorRole,
annotatorName,
annotationTime,
colorAddition,
colorDeletion,
isLast = false,
}) => (
<>
<Divider />
<SectionContainer isLast={isLast}>
<TypeText tx={annotatorRole} />
<AnnotatorInformationContainer>
<InformationText tx={annotatorName} />
<CircleContainer>
<EditCircle circleColor={colorAddition}>
<Icon icon="pixelBrush" />
</EditCircle>
{colorDeletion && (
<EditCircle circleColor={colorDeletion}>
<Icon icon="eraser" />
</EditCircle>
)}
</CircleContainer>
</AnnotatorInformationContainer>
{annotationTime && (
<AnnotationTimeContainer>
<TypeText tx="annotation-time" />
<InformationText tx={annotationTime} />
</AnnotationTimeContainer>
)}
</SectionContainer>
</>
);

export const SupervisorPanel = observer(() => {
const store = useStore();
if (!(store?.currentTask?.kind === TaskType.Review)) return <></>;
const annotationCount = store.currentTask.annotations.length;
const annotations = store.currentTask.annotations.sort(
(firstAnnotation, secondAnnotation) =>
new Date(firstAnnotation.submittedAt).getTime() -
new Date(secondAnnotation.submittedAt).getTime(),
);

const [shouldShowDelta, setShouldShowDelta] = useState(false);

return (
<Modal>
{/* TODO: Set delta options */}
<BooleanParam
labelTx="show-delta"
value={shouldShowDelta}
setValue={() => {
setShouldShowDelta(!shouldShowDelta);
}}
/>
{annotations.map((annotation, index) => {
const isLast = index === annotationCount - 1;
// TODO: Make coloring work properly
const correspondingLayer = store.editor.activeDocument?.getLayer(
annotation.data[0].correspondingLayerId,
);
const annotationColor = correspondingLayer?.color || "yellow";
return (
<AnnotatorSection
key={index}
annotatorRole={annotation.annotator.getRoleName()}
annotatorName={annotation.annotator.username}
colorAddition={annotationColor}
colorDeletion={
annotation.annotator.getRoleName() === "Reviewer" ? "red" : ""
}
isLast={isLast}
// TODO: Remove mocked annotation time (needs to be added to the API for this)
annotationTime={isLast ? "01:12:26" : ""}
/>
);
})}
</Modal>
);
});
6 changes: 6 additions & 0 deletions apps/editor/src/components/editor/ui-overlay/ui-overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
FloatingUIButton,
Notification,
Spacer,
TaskType,
Text,
} from "@visian/ui-shared";
import { isFromWHO } from "@visian/utils";
Expand Down Expand Up @@ -37,6 +38,7 @@ import { ViewSettings } from "../view-settings";
import { UIOverlayProps } from "./ui-overlay.props";
import { SettingsPopUp } from "../settings-popup";
import { MeasurementPopUp } from "../measurement-popup";
import { SupervisorPanel } from "../supervisor-panel";

const Container = styled(AbsoluteCover)`
align-items: stretch;
Expand Down Expand Up @@ -245,6 +247,10 @@ export const UIOverlay = observer<UIOverlayProps>(
<ThresholdAnnotationModal />
<DilateErodeModal />
<MeasurementModal />
{isFromWHO() &&
store?.currentTask?.kind === TaskType.Review && (
<SupervisorPanel />
)}
<AxesAndVoxel />
</ModalRow>
</ColumnLeft>
Expand Down
5 changes: 5 additions & 0 deletions apps/editor/src/models/editor/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ErrorNotification,
ValueType,
PerformanceMode,
ITask,
} from "@visian/ui-shared";
import {
handlePromiseSettledResult,
Expand Down Expand Up @@ -824,6 +825,10 @@ export class Document
return this.editor.performanceMode;
}

public get currentTask(): ITask | undefined {
return this.context?.getCurrentTask();
}

// Serialization
public toJSON(): DocumentSnapshot {
return {
Expand Down
17 changes: 12 additions & 5 deletions apps/editor/src/models/editor/tools/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {
IconType,
IDocument,
ITool,
TaskType,
ViewMode,
} from "@visian/ui-shared";
import { ISerializable } from "@visian/utils";
import { ISerializable, isFromWHO } from "@visian/utils";
import { makeObservable, observable } from "mobx";

import { Parameter, ParameterSnapshot } from "../parameters";
Expand Down Expand Up @@ -96,11 +97,17 @@ export class Tool<N extends string>
}

public canActivate(): boolean {
const isSupervisorMode =
isFromWHO() && this.document.currentTask?.kind === TaskType.Review;
return Boolean(
(!this.supportedViewModes ||
this.supportedViewModes.includes(
this.document.viewSettings.viewMode,
)) &&
!(
isSupervisorMode &&
(this.isDrawingTool || this.supportAnnotationsOnly)
) &&
(!this.supportedViewModes ||
this.supportedViewModes.includes(
this.document.viewSettings.viewMode,
)) &&
(!this.supportedLayerKinds ||
(this.document.activeLayer &&
this.supportedLayerKinds.includes(
Expand Down
32 changes: 21 additions & 11 deletions apps/editor/src/models/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
IStorageBackend,
Tab,
ErrorNotification,
TaskType,
ITask,
} from "@visian/ui-shared";
import {
createFileFromBase64,
Expand All @@ -21,7 +23,7 @@ import { DICOMWebServer } from "./dicomweb-server";
import { Editor, EditorSnapshot } from "./editor";
import { Tracker } from "./tracking";
import { ProgressNotification } from "./types";
import { Task, TaskType } from "./who";
import { Task } from "./who";

export interface RootSnapshot {
editor: EditorSnapshot;
Expand Down Expand Up @@ -54,7 +56,7 @@ export class RootStore implements ISerializable<RootSnapshot>, IDisposable {
public refs: { [key: string]: React.RefObject<HTMLElement> } = {};
public pointerDispatch?: IDispatch;

public currentTask?: Task;
public currentTask?: ITask;

public tracker?: Tracker;

Expand Down Expand Up @@ -96,6 +98,7 @@ export class RootStore implements ISerializable<RootSnapshot>, IDisposable {
setError: this.setError,
getTracker: () => this.tracker,
getColorMode: () => this.colorMode,
getCurrentTask: () => this.currentTask,
});

deepObserve(this.editor, this.persist, {
Expand Down Expand Up @@ -194,24 +197,31 @@ export class RootStore implements ISerializable<RootSnapshot>, IDisposable {
} else {
// Task Type is Correct or Review
await Promise.all(
whoTask.annotations.map(async (annotation, index) => {
const title =
whoTask.samples[index].title ||
whoTask.samples[0].title ||
`annotation_${index}`;
whoTask.annotations.map(async (annotation, annotationIndex) => {
let baseTitle =
whoTask.samples[annotationIndex]?.title ||
whoTask.samples[0]?.title ||
// TODO: Translation for unnamed
"unnamed.nii";
// TODO: Handle base title without ".nii" ending
baseTitle = baseTitle.replace(
".nii",
`_annotation_${annotationIndex}`,
);

await Promise.all(
annotation.data.map(async (annotationData) => {
annotation.data.map(async (annotationData, dataIndex) => {
const title = `${baseTitle}_${dataIndex}`;
const createdLayerId = await this.editor.activeDocument?.importFiles(
createFileFromBase64(
title.replace(".nii", "_annotation").concat(".nii"),
title.concat(".nii"),
annotationData.data,
),
title.replace(".nii", "_annotation"),
title,
true,
);
if (createdLayerId)
annotationData.correspondingLayerId = createdLayerId;
annotationData.setCorrespondingLayerId(createdLayerId);
}),
);
}),
Expand Down
9 changes: 8 additions & 1 deletion apps/editor/src/models/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type React from "react";
import type { ColorMode, ErrorNotification, Theme } from "@visian/ui-shared";
import type {
ColorMode,
ErrorNotification,
ITask,
Theme,
} from "@visian/ui-shared";

import type { Tracker } from "./tracking";

Expand Down Expand Up @@ -27,6 +32,8 @@ export interface StoreContext {
getTracker(): Tracker | undefined;

getColorMode(): ColorMode;

getCurrentTask(): ITask | undefined;
}

export interface ProgressNotification {
Expand Down
Loading