From cd2796e3c0d8294ddfea406d666174ecb0c8fb2e Mon Sep 17 00:00:00 2001 From: kieftrav Date: Tue, 10 Dec 2024 08:24:47 -0600 Subject: [PATCH] VV - Implement Classifier Tool/Task (#6511) * Create Volumetric Task and stub out Annotation --- .../TaskInputLabel/TaskInputLabel.js | 3 +- .../tasks/experimental/volumetric/README.md | 11 +++ .../volumetric/components/VolumetricTask.js | 95 +++++++++++++++++++ .../components/VolumetricTask.spec.js | 24 +++++ .../components/VolumetricTask.stories.js | 23 +++++ .../tasks/experimental/volumetric/index.js | 9 ++ .../volumetric/models/VolumetricAnnotation.js | 11 +++ .../volumetric/models/VolumetricTask.js | 23 +++++ .../volumetric/models/VolumetricTask.spec.js | 77 +++++++++++++++ .../lib-classifier/src/plugins/tasks/index.js | 1 + .../src/store/Project/Project.js | 2 +- 11 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 packages/lib-classifier/src/plugins/tasks/experimental/volumetric/README.md create mode 100644 packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.js create mode 100644 packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.spec.js create mode 100644 packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.stories.js create mode 100644 packages/lib-classifier/src/plugins/tasks/experimental/volumetric/index.js create mode 100644 packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricAnnotation.js create mode 100644 packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.js create mode 100644 packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.spec.js diff --git a/packages/lib-classifier/src/plugins/tasks/components/TaskInput/components/TaskInputLabel/TaskInputLabel.js b/packages/lib-classifier/src/plugins/tasks/components/TaskInput/components/TaskInputLabel/TaskInputLabel.js index 6277e327c0..37baf8d832 100644 --- a/packages/lib-classifier/src/plugins/tasks/components/TaskInput/components/TaskInputLabel/TaskInputLabel.js +++ b/packages/lib-classifier/src/plugins/tasks/components/TaskInput/components/TaskInputLabel/TaskInputLabel.js @@ -15,8 +15,7 @@ const StyledTaskInputLabelWrapper = styled.span` ` const StyledText = styled(Text)` - display: block; - margin: 1em 0; + align-content: center; padding-left: 15px; padding-right: 15px; diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/README.md b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/README.md new file mode 100644 index 0000000000..86e10d36dd --- /dev/null +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/README.md @@ -0,0 +1,11 @@ +# Volumetric Task + +The volumetric task is a task type designed for use with structured volumetric JSON, as part of an NIH neuron mapping project. The task is a drawing/click-oriented task that constructs volumetric annotation data based on the clicked point in 3D space. + +The volumetric task requires subjects with a location of application/json mime type. The subject's location content is used to initialize the structured subject data to be annotated. + +The volumetric task does not support a required property. + +The volumetric task does not support tags (i.e. insertion, deletion). + +The volumetric task is disabled until the subject is loaded. diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.js new file mode 100644 index 0000000000..b77a3444df --- /dev/null +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.js @@ -0,0 +1,95 @@ +import { Box, Text } from "grommet" +import { Blank } from "grommet-icons" +import InputStatus from "../../../components/InputStatus" +import { Markdownz } from "@zooniverse/react-components" +import { observer } from "mobx-react" +import PropTypes from "prop-types" +import styled from "styled-components" +import TaskInput from "../../../components/TaskInput" + +// Note: ANNOTATION_COUNT will be refactored in next PR to use MobX Annotations +const ANNOTATION_COUNT = 3 +const SVG_ARROW = "48 50, 48 15, 40 15, 50 0, 60 15, 52 15, 52 50" + +const StyledInstructionText = styled(Text)` + margin: 0; + padding: 0; + width: 100%; + + > *:first-child { + margin-top: 0; + } +` + +const StyledToolIcon = styled.div` + background-color: #2d2d2d; + display: flex; + align-items: center; + padding-left: 15px; + + &::after { + content: " "; + margin-right: 1ch; + white-space: pre; + } + + > svg { + height: 1.5em; + vertical-align: bottom; + width: 1.5em; + } +` + +function VolumetricTask({ disabled = false, task }) { + return ( + + + {task.instruction} + + + {/* + NOTE: Because there is only one active tool in a Volumetric project, + we do checked=true & index=0 to hardcode this tool as active + */} + + + + + + + + + } + labelStatus={} + name="volumetric-tool" + type="radio" + /> + + ) +} + +VolumetricTask.propTypes = { + task: PropTypes.shape({ + instruction: PropTypes.string, + }), +} + +export default observer(VolumetricTask) diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.spec.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.spec.js new file mode 100644 index 0000000000..624e2cf282 --- /dev/null +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.spec.js @@ -0,0 +1,24 @@ +import { composeStory } from "@storybook/react" +import { render, screen } from "@testing-library/react" +import Meta, { Default } from "./VolumetricTask.stories" + +describe("VolumetricTask", function () { + const DefaultStory = composeStory(Default, Meta) + + describe("when it renders", function () { + it("should show the instruction", function () { + render() + expect(screen.getByText("Volumetric the task")).to.be.ok() + }) + + it("should show the label text", function () { + render() + expect(screen.getByText("3D Viewer")).to.be.ok() + }) + + it("should show the classification count", function () { + render() + expect(screen.getByText("InputStatus.drawn")).to.be.ok() + }) + }) +}) diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.stories.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.stories.js new file mode 100644 index 0000000000..b8d7fa9e53 --- /dev/null +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/components/VolumetricTask.stories.js @@ -0,0 +1,23 @@ +import { MockTask } from '@stories/components' +import VolumetricTask from './VolumetricTask' + +export default { + title: 'Tasks / Volumetric', + component: VolumetricTask, + args: {}, + argTypes: {} +} + +const tasks = { + T0: { + strings: { + instruction: 'Volumetric the task', + }, + taskKey: 'T0', + type: 'volumetric', + } +} + +export function Default () { + return () +} diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/index.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/index.js new file mode 100644 index 0000000000..02a76290f6 --- /dev/null +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/index.js @@ -0,0 +1,9 @@ +import { default as TaskComponent } from "./components/VolumetricTask" +import { default as TaskModel } from "./models/VolumetricTask" +import { default as AnnotationModel } from "./models/VolumetricAnnotation" + +export default { + TaskComponent, + TaskModel, + AnnotationModel +} diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricAnnotation.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricAnnotation.js new file mode 100644 index 0000000000..7587fa958d --- /dev/null +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricAnnotation.js @@ -0,0 +1,11 @@ +import { types } from 'mobx-state-tree' +import Annotation from '../../../models/Annotation' + +const Volumetric = types + .model('Volumetric', { + taskType: types.literal('volumetric'), + value: types.optional(types.string, ''), + }) + +const VolumetricAnnotation = types.compose('VolumetricAnnotation', Annotation, Volumetric) +export default VolumetricAnnotation diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.js new file mode 100644 index 0000000000..2fe7735557 --- /dev/null +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.js @@ -0,0 +1,23 @@ +import cuid from "cuid" +import { types } from "mobx-state-tree" +import Task from "../../../models/Task" +import VolumetricAnnotation from "./VolumetricAnnotation" + +const Volumetric = types + .model("Volumetric", { + annotation: types.safeReference(VolumetricAnnotation), + type: types.literal("volumetric"), + }) + .views((self) => ({ + defaultAnnotation(id = cuid()) { + return VolumetricAnnotation.create({ + id, + task: self.taskKey, + taskType: self.type, + }) + }, + })) + +const VolumetricTask = types.compose("VolumetricTask", Task, Volumetric) + +export default VolumetricTask diff --git a/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.spec.js b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.spec.js new file mode 100644 index 0000000000..dbc2d3b7e4 --- /dev/null +++ b/packages/lib-classifier/src/plugins/tasks/experimental/volumetric/models/VolumetricTask.spec.js @@ -0,0 +1,77 @@ +import VolumetricTask from "@plugins/tasks/experimental/volumetric" + +describe("Model > VolumetricTask", function () { + const volumetricTask = { + taskKey: "T0", + type: "volumetric", + } + + const singleChoiceTask = { + answers: [ + { label: "yes", next: "S2" }, + { label: "no", next: "S3" }, + ], + strings: { + question: "Do you exist?", + }, + required: "", + taskKey: "T1", + type: "single", + } + + it("should exist", function () { + const task = VolumetricTask.TaskModel.create(volumetricTask) + expect(task).to.be.ok() + expect(task).to.be.an("object") + }) + + it("should error for invalid tasks", function () { + let errorThrown = false + try { + VolumetricTask.TaskModel.create(singleChoiceTask) + } catch (e) { + errorThrown = true + } + expect(errorThrown).to.be.true() + }) + + describe("Views > defaultAnnotation", function () { + let task + + before(function () { + task = VolumetricTask.TaskModel.create(volumetricTask) + }) + + it("should be a valid annotation", function () { + const annotation = task.defaultAnnotation() + expect(annotation.id).to.be.ok() + expect(annotation.task).to.equal("T0") + expect(annotation.taskType).to.equal("volumetric") + }) + + it("should generate unique annotations", function () { + const firstAnnotation = task.defaultAnnotation() + const secondAnnotation = task.defaultAnnotation() + expect(firstAnnotation.id).to.not.equal(secondAnnotation.id) + }) + }) + + describe("with an annotation", function () { + let annotation + let task + + before(function () { + task = VolumetricTask.TaskModel.create(volumetricTask) + annotation = task.defaultAnnotation() + }) + + it("should start up with an empty string", function () { + expect(annotation.value).to.equal("") + }) + + it("should update annotations", function () { + annotation.update("Hello there!") + expect(annotation.value).to.equal("Hello there!") + }) + }) +}) diff --git a/packages/lib-classifier/src/plugins/tasks/index.js b/packages/lib-classifier/src/plugins/tasks/index.js index 7ab9979c28..1ecf1d3052 100644 --- a/packages/lib-classifier/src/plugins/tasks/index.js +++ b/packages/lib-classifier/src/plugins/tasks/index.js @@ -10,3 +10,4 @@ export { default as text } from './text' export { default as highlighter } from './experimental/highlighter' export { default as textFromSubject } from './experimental/textFromSubject' export { default as transcription } from './experimental/transcription' +export { default as volumetric } from './experimental/volumetric' diff --git a/packages/lib-classifier/src/store/Project/Project.js b/packages/lib-classifier/src/store/Project/Project.js index 6fc0a93e90..0da522ef32 100644 --- a/packages/lib-classifier/src/store/Project/Project.js +++ b/packages/lib-classifier/src/store/Project/Project.js @@ -26,7 +26,7 @@ const Project = types }, get isVolumetricViewer() { - return self.experimental_tools.includes('volumetricViewer') + return self.experimental_tools.includes('volumetricProject') }, }))