diff --git a/package.json b/package.json index dbb973f..d62e6e6 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,14 @@ { "name": "react-nlp-annotate", - "version": "0.1.21", - "homepage": "https://workaroundonline.github.io/react-nlp-annotate/", - "dependencies": {}, + "version": "0.2.0", + "homepage": "https://waoai.github.io/react-nlp-annotate/", + "dependencies": { + "react-hotkeys": "^2.0.0", + "react-material-workspace-layout": "^0.1.6", + "use-event-callback": "^0.1.0" + }, "scripts": { - "build": "npm run build:babel && cp ./package.json ./dist/package.json", + "build": "rimraf dist && npm run build:babel && cp ./package.json ./dist/package.json", "build:babel": "NODE_ENV=production babel ./src --out-dir=./dist", "release": "npm run build && cd dist && npm publish", "storybook": "start-storybook -p 9050 -s public", @@ -20,16 +24,19 @@ "not ie <= 11", "not op_mini all" ], + "eslintConfig": { + "extends": "react-app" + }, "devDependencies": { "@babel/cli": "^7.2.3", "@babel/core": "^7.2.2", - "@material-ui/core": "^3.9.2", - "@material-ui/icons": "^3.0.2", - "@material-ui/styles": "^3.0.0-alpha.10", - "@storybook/addon-actions": "^4", - "@storybook/addon-links": "^4", - "@storybook/addons": "^4", - "@storybook/react": "^4", + "@material-ui/core": "^4.10.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/styles": "^4.10.0", + "@storybook/addon-actions": "^5.3.19", + "@storybook/addon-links": "^5.3.19", + "@storybook/addons": "^5.3.19", + "@storybook/react": "^5.3.19", "axios": "^0.19.0", "chroma-js": "^2.0.3", "downloadjs": "^1.4.7", @@ -38,12 +45,13 @@ "js-base64": "^2.5.1", "lodash": "^4.17.11", "query-string": "^6.8.1", - "react": "16.8.0-alpha.1", - "react-dom": "16.8.0-alpha.1", + "react": "^16.13.1", + "react-dom": "^16.13.1", "react-monaco-editor": "^0.26.2", "react-scripts": "2.1.3", "react-select": "^3.0.8", "react-syntax-highlighter": "^10.3.0", + "rimraf": "^3.0.2", "spelling": "^2.0.1" } } diff --git a/src/components/Container/index.js b/src/components/Container/index.js index a1d596b..ed46a99 100644 --- a/src/components/Container/index.js +++ b/src/components/Container/index.js @@ -1,9 +1,50 @@ // @flow -import React from "react" +import React, { useMemo } from "react" +import Box from "@material-ui/core/Box" +import Typography from "@material-ui/core/Typography" +import Workspace from "react-material-workspace-layout/Workspace" -export default ({ children }: any) => ( -
- {children} -
-) +export default ({ + children, + onNext, + onPrev, + currentSampleIndex = 0, + numberOfSamples = 1, + titleContent, + onClickHeaderItem +}: any) => { + const headerItems = useMemo( + () => + [ + (currentSampleIndex > 0 || onPrev) && { name: "Prev", onClick: onPrev }, + (numberOfSamples > currentSampleIndex + 1 || onNext) && { + name: "Next", + onClick: onNext + }, + { name: "Done" } + ].filter(Boolean), + [currentSampleIndex, numberOfSamples] + ) + return ( + + + Sample {currentSampleIndex + 1} / {numberOfSamples} + + + ) : ( + titleContent + ) + } + onClickHeaderItem={onClickHeaderItem} + headerItems={headerItems} + iconSidebarItems={[]} + rightSidebarItems={[]} + > + {children} + + ) +} diff --git a/src/components/Container/index.story.js b/src/components/Container/index.story.js new file mode 100644 index 0000000..6d45d9c --- /dev/null +++ b/src/components/Container/index.story.js @@ -0,0 +1,16 @@ +// @flow + +import React from "react" +import { storiesOf } from "@storybook/react" +import { action } from "@storybook/addon-actions" +import Container from "./" + +storiesOf("Container", module).add("Basic", () => ( + + Some inner content + +)) diff --git a/src/components/Document/index.js b/src/components/Document/index.js index 0e34b2e..d5d1620 100644 --- a/src/components/Document/index.js +++ b/src/components/Document/index.js @@ -21,13 +21,14 @@ export default function Document({ sequence, onHighlightedChanged = () => null, onSequenceChange = () => null, - nothingHighlighted, + nothingHighlighted = false, colorLabelMap = {} }: Props) { const [mouseDown, changeMouseDown] = useState() - const [[firstSelected, lastSelected], changeHighlightedRangeState] = useState( - [null, null] - ) + const [ + [firstSelected, lastSelected], + changeHighlightedRangeState + ] = useState([null, null]) const changeHighlightedRange = ([first, last]) => { changeHighlightedRangeState([first, last]) const highlightedItems = [] @@ -51,89 +52,88 @@ export default function Document({ onMouseUp={() => changeMouseDown(false)} > {sequence.map((seq, i) => ( - <> - { - if (seq.label) return - changeHighlightedRange([i, i]) - }} - onMouseMove={() => { - if (seq.label) return - if (mouseDown && i !== lastSelected) { - changeHighlightedRange([ - firstSelected === null ? i : firstSelected, - i - ]) - } - }} - style={ - seq.label - ? { - display: "inline-flex", - backgroundColor: - seq.color || colorLabelMap[seq.label] || "#333", - color: "#fff", - padding: 4, - margin: 4, - paddingLeft: 10, - paddingRight: 10, - borderRadius: 4, - userSelect: "none" - } - : { - display: "inline-flex", - backgroundColor: - seq.text !== " " && highlightedItems.includes(i) - ? "#ccc" - : "inherit", - color: "#333", - marginTop: 4, - marginBottom: 4, - paddingTop: 4, - paddingBottom: 4, - paddingLeft: 2, - paddingRight: 2, - userSelect: "none" - } + { + if (seq.label) return + changeHighlightedRange([i, i]) + }} + onMouseMove={() => { + if (seq.label) return + if (mouseDown && i !== lastSelected) { + changeHighlightedRange([ + firstSelected === null ? i : firstSelected, + i + ]) } - key={i} - > - {seq.label ? ( - -
{seq.text}
-
- ) : ( -
{seq.text}
- )} - {seq.label && ( -
{ - onSequenceChange( - sequence - .flatMap(s => (s !== seq ? s : stringToSequence(s.text))) - .filter(s => s.text.length > 0) - ) - }} - style={{ + }} + style={ + seq.label + ? { display: "inline-flex", - cursor: "pointer", - alignSelf: "center", - fontSize: 11, - width: 18, - height: 18, - alignItems: "center", - justifyContent: "center", - marginLeft: 4, - borderRadius: 9, + backgroundColor: + seq.color || colorLabelMap[seq.label] || "#333", color: "#fff", - backgroundColor: "rgba(0,0,0,0.2)" - }} - > - {"\u2716"} -
- )} -
- + padding: 4, + margin: 4, + paddingLeft: 10, + paddingRight: 10, + borderRadius: 4, + userSelect: "none" + } + : { + display: "inline-flex", + backgroundColor: + seq.text !== " " && highlightedItems.includes(i) + ? "#ccc" + : "inherit", + color: "#333", + marginTop: 4, + marginBottom: 4, + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 2, + paddingRight: 2, + userSelect: "none" + } + } + key={i} + > + {seq.label ? ( + +
{seq.text}
+
+ ) : ( +
{seq.text}
+ )} + {seq.label && ( +
{ + onSequenceChange( + sequence + .flatMap(s => (s !== seq ? s : stringToSequence(s.text))) + .filter(s => s.text.length > 0) + ) + }} + style={{ + display: "inline-flex", + cursor: "pointer", + alignSelf: "center", + fontSize: 11, + width: 18, + height: 18, + alignItems: "center", + justifyContent: "center", + marginLeft: 4, + borderRadius: 9, + color: "#fff", + backgroundColor: "rgba(0,0,0,0.2)" + }} + > + {"\u2716"} +
+ )} +
))} ) diff --git a/src/components/DocumentLabeler/index.js b/src/components/DocumentLabeler/index.js index 18e105d..4683dd7 100644 --- a/src/components/DocumentLabeler/index.js +++ b/src/components/DocumentLabeler/index.js @@ -19,8 +19,10 @@ export default function DocumentLabeler(props: LabelDocumentProps) {
{ + console.log({ labelId }) if (props.multipleLabels) { changeSelectedLabels(selectedLabels.concat([labelId])) props.onChange(selectedLabels.concat([labelId])) @@ -38,6 +40,7 @@ export default function DocumentLabeler(props: LabelDocumentProps) { if (!label) return return ( ( )) .add("Basic with Initial Label", () => ( )) .add("Basic Multi Label", () => ( )) .add("Basic Nested Labels", () => ( + )) + .add("Basic Nested Labels without hotkeys", () => ( + )) diff --git a/src/components/EditableDocument/index.js b/src/components/EditableDocument/index.js index a887ca9..e4bc50e 100644 --- a/src/components/EditableDocument/index.js +++ b/src/components/EditableDocument/index.js @@ -93,7 +93,10 @@ export default function EditableDocument({ const handleChange = v => { if (!v) v = [] changeValue(v) - const result = v.map(l => l.label).join(" ") + const result = v + .map(l => l.label.trim()) + .join(" ") + .trim() try { changeValidationErrors(validator(result)) } catch (e) { @@ -101,13 +104,43 @@ export default function EditableDocument({ } onChange(result) } + const isInDictionary = text => { + if (lowerCaseMode) text = text.trim().toLowerCase() + const scRes = spellChecker.lookup(text) + if (scRes.found || phraseBank.includes(text)) return true + return false + } + const handleInputChange = v => changeInputValue(v) const handleKeyDown = ({ key }) => { if (!inputValue) return if (key === "Enter" || key === "Tab") { changeValue([ ...(value || []), - createOption(inputValue + " ", yellow[700]) + createOption(inputValue.trim(), yellow[700]) + ]) + changeInputValue("") + } else if (key === " " && isInDictionary(inputValue.trim())) { + changeValue([ + ...(value || []), + createOption(inputValue.trim(), green[500]) + ]) + changeInputValue("") + } else if ( + key === " " && + isInDictionary(inputValue.split(" ").slice(-1)[0]) + ) { + changeValue([ + ...(value || []), + createOption( + inputValue + .split(" ") + .slice(0, -1) + .join(" ") + .trim(), + yellow[700] + ), + createOption(inputValue.split(" ").slice(-1)[0], green[700]) ]) changeInputValue("") } diff --git a/src/components/LabelButton/index.js b/src/components/LabelButton/index.js index aadeea8..b99e2bc 100644 --- a/src/components/LabelButton/index.js +++ b/src/components/LabelButton/index.js @@ -1,10 +1,11 @@ // @flow -import React from "react" +import React, { useMemo } from "react" import type { Label as LabelType } from "../../types.js" import FolderOpenIcon from "@material-ui/icons/FolderOpen" import classnames from "classnames" -import makeStyles from "@material-ui/styles/makeStyles" +import { makeStyles } from "@material-ui/core/styles" +import Tooltip from "@material-ui/core/Tooltip" const useStyles = makeStyles({ label: { @@ -25,6 +26,26 @@ const useStyles = makeStyles({ fontSize: 12, fontWeight: "bold" } + }, + deleteableIcon: { + display: "inline-flex", + cursor: "pointer", + alignSelf: "center", + fontSize: 11, + width: 18, + height: 18, + alignItems: "center", + justifyContent: "center", + marginLeft: 4, + borderRadius: 9, + color: "#fff", + backgroundColor: "rgba(0,0,0,0.2)" + }, + hotkeyText: { + paddingLeft: 4 + }, + tooltip: { + whiteSpace: "pre-wrap" } }) @@ -32,7 +53,7 @@ const Label = (props: { ...$Exact, hasChildren?: boolean, small?: boolean, - hotkey?: string, + hotkey?: ?string, deletable?: boolean, onClick: string => any }) => { @@ -48,8 +69,11 @@ const Label = (props: { deletable } = props const classes = useStyles() + const tooltipClasses = useMemo(() => ({ tooltip: classes.tooltip }), [ + classes + ]) - return ( + const button = (
props.onClick(id)} className={classnames(classes.label, small && "small")} @@ -65,29 +89,24 @@ const Label = (props: { /> )}
{displayName || id}
- {hotkey &&
({hotkey})
} + {hotkey &&
({hotkey})
} {deletable && ( -
+
{"\u2716"}
)}
) + + if (description) { + return ( + + {button} + + ) + } else { + return button + } } export default Label diff --git a/src/components/LabelButton/index.story.js b/src/components/LabelButton/index.story.js index 7555b1d..47c2312 100644 --- a/src/components/LabelButton/index.story.js +++ b/src/components/LabelButton/index.story.js @@ -4,7 +4,15 @@ import React from "react" import { storiesOf } from "@storybook/react" import { action } from "@storybook/addon-actions" +import colors from "../../colors" import LabelButton from "./" -storiesOf("LabelButton", module).add("Basic", () => ) +storiesOf("LabelButton", module).add("Basic", () => ( + +)) diff --git a/src/components/LabelSelector/index.js b/src/components/LabelSelector/index.js index 1a28e6d..c66af6d 100644 --- a/src/components/LabelSelector/index.js +++ b/src/components/LabelSelector/index.js @@ -1,16 +1,9 @@ // @flow -import React, { useState, useLayoutEffect, useEffect } from "react" -import { makeStyles } from "@material-ui/styles" +import React, { useState, useEffect, useMemo } from "react" import type { Label as LabelType } from "../../types.js" import LabelButton from "../LabelButton" -import Tooltip from "@material-ui/core/Tooltip" - -const useStyles = makeStyles({ - tooltip: { - whiteSpace: "pre-wrap" - } -}) +import colors from "../../colors" const findRouteFromParents = (labelId, labels) => { if (!labelId) return [] @@ -23,7 +16,8 @@ const findRouteFromParents = (labelId, labels) => { export default ({ labels, - onSelectLabel + onSelectLabel, + hotkeysEnabled = false }: { labels: Array, onSelectLabel: string => any @@ -46,7 +40,8 @@ export default ({ } } - useLayoutEffect(() => { + useEffect(() => { + if (!hotkeysEnabled) return const eventFunc = e => { if (hotkeyLabelMap[e.key]) { const labelId = hotkeyLabelMap[e.key] @@ -66,9 +61,7 @@ export default ({ return () => { window.removeEventListener("keydown", eventFunc) } - }) - - const c = useStyles() + }, [changeParents, onSelectLabel, hotkeysEnabled]) return (
@@ -91,22 +84,14 @@ export default ({ hasChildren: labels.some(l2 => l2.parent === l.id) })) .map((l, i) => ( - - { - changeParents(parents.slice(0, parents.indexOf(l.id) + 1)) - }} - /> - + { + changeParents(parents.slice(0, parents.indexOf(l.id) + 1)) + }} + /> ))}
)} @@ -118,26 +103,18 @@ export default ({ hasChildren: labels.some(l2 => l2.parent === l.id) })) .map((l, i) => ( - -
- { - changeParents(parents.concat([l.id])) - } - } - /> -
-
+ { + changeParents(parents.concat([l.id])) + } + } + /> ))}
diff --git a/src/components/LabelSelector/index.story.js b/src/components/LabelSelector/index.story.js index 2f9cb01..99a0acf 100644 --- a/src/components/LabelSelector/index.story.js +++ b/src/components/LabelSelector/index.story.js @@ -4,7 +4,48 @@ import React from "react" import { storiesOf } from "@storybook/react" import { action } from "@storybook/addon-actions" +import colors from "../../colors" import LabelSelector from "./" -storiesOf("LabelSelector", module).add("Basic", () => ) +storiesOf("LabelSelector", module) + .add("Basic", () => ( + + )) + .add("Hotkeys Off", () => ( + + )) diff --git a/src/components/NLPAnnotator/index.js b/src/components/NLPAnnotator/index.js index e1aab4f..c3415b5 100644 --- a/src/components/NLPAnnotator/index.js +++ b/src/components/NLPAnnotator/index.js @@ -1,36 +1,19 @@ // @flow -import React, { useState, useLayoutEffect } from "react" +import React, { useState, useEffect, useMemo } from "react" import type { NLPAnnotatorProps } from "../../types" import SequenceAnnotator from "../SequenceAnnotator" import DocumentLabeler from "../DocumentLabeler" import Transcriber from "../Transcriber" import colors from "../../colors" -import { green } from "@material-ui/core/colors" -import makeStyles from "@material-ui/styles/makeStyles" import Container from "../Container" -import Button from "@material-ui/core/Button" +import useEventCallback from "use-event-callback" -const useStyles = makeStyles({ - finishButton: { - "&&": { - fontSize: 14, - textTransform: "none", - backgroundColor: green[500], - padding: 10, - color: "#fff", - margin: 10, - fontWeight: "bold", - "&:hover": { - backgroundColor: green[700] - } - } - } -}) +const defaultValidator = () => [] export default function NLPAnnotator(props: NLPAnnotatorProps) { - const classes = useStyles() + const validator = props.validator || defaultValidator let [output, changeOutput] = useState(null) if (output === null && props.type === "transcribe") { @@ -40,7 +23,7 @@ export default function NLPAnnotator(props: NLPAnnotatorProps) { output = props.initialSequence || [{ text: props.document }] } - useLayoutEffect(() => { + useEffect(() => { const eventFunc = e => { if (e.key === "Enter") { if (props.onFinish) props.onFinish(output) @@ -50,63 +33,72 @@ export default function NLPAnnotator(props: NLPAnnotatorProps) { return () => { window.removeEventListener("keydown", eventFunc) } - }) + }, [props.onFinish, output]) const onChange = (newOutput: any) => { if (props.onChange) props.onChange(newOutput) changeOutput(newOutput) } - if (props.labels && (props: any).labels.some(l => !l.color)) { - props = ({ - ...props, - labels: (props: any).labels.map((l, i) => ({ - color: colors[i % colors.length], - ...l + + let labels = useMemo(() => { + let labels = props.labels || [] + if (!labels.some(l => !l.color)) { + labels = labels.map((l, i) => ({ + ...l, + color: colors[i % colors.length] })) - }: any) - } - let finishButton = null - if (props.onFinish) { - finishButton = ( - - ) - } - if (props.type === "label-sequence") { - return ( - - -
{finishButton}
-
- ) - } - if (props.type === "label-document") { - return ( - - -
{finishButton}
-
- ) - } - if (props.type === "transcribe") { - return ( - - -
{finishButton}
-
- ) + } + return labels + }, [props.labels]) + + const isPassingValidation = !validator(output).some(msg => + msg.toLowerCase().includes("error") + ) + + console.log({ output }) + const onFinish = useEventCallback(() => { + if (!isPassingValidation) return + console.log("onFinish", output) + props.onFinish(output) + }) + + const onClickHeaderItem = useEventCallback(({ name }) => { + switch (name) { + case "Done": + onFinish(output) + return + default: + return + } + }) + + let annotator + switch (props.type) { + case "label-sequence": + annotator = ( + + ) + break + case "label-document": + annotator = ( + + ) + break + case "transcribe": + annotator = + break + default: + annotator = null } - return null + + return ( + +
{annotator}
+
+ ) } diff --git a/src/components/NLPAnnotator/index.story.js b/src/components/NLPAnnotator/index.story.js index 286910d..96d3579 100644 --- a/src/components/NLPAnnotator/index.story.js +++ b/src/components/NLPAnnotator/index.story.js @@ -11,7 +11,11 @@ import NLPAnnotator from "./" storiesOf("NLPAnnotator", module) .add("Sequence Labeler", () => ( ( ( ( t.length === 0 ? ["Error: Must be some text"] : [] } + onChange={action("onChange")} onFinish={action("onFinish")} /> )) diff --git a/src/components/SequenceAnnotator/index.js b/src/components/SequenceAnnotator/index.js index da20b50..1f249c0 100644 --- a/src/components/SequenceAnnotator/index.js +++ b/src/components/SequenceAnnotator/index.js @@ -31,6 +31,7 @@ export default function SequenceAnnotator(props: SequenceAnnotatorProps) {
{ const { color } = props.labels.find(({ id }) => label === id) || {} diff --git a/src/components/Theme/index.js b/src/components/Theme/index.js index c128687..9b0e2e6 100644 --- a/src/components/Theme/index.js +++ b/src/components/Theme/index.js @@ -2,8 +2,7 @@ import React from "react" import { makeStyles } from "@material-ui/styles" -import createMuiTheme from "@material-ui/core/styles/createMuiTheme" -import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider" +import { ThemeProvider, createMuiTheme } from "@material-ui/core/styles" import "./theme.css" const useStyles = makeStyles({ @@ -28,8 +27,8 @@ const theme = createMuiTheme({ export default ({ children }: any) => { const classes = useStyles() return ( - +
{children}
-
+ ) } diff --git a/src/components/Transcriber/index.story.js b/src/components/Transcriber/index.story.js index f393669..e5fd578 100644 --- a/src/components/Transcriber/index.story.js +++ b/src/components/Transcriber/index.story.js @@ -13,6 +13,7 @@ const externalWordBank = storiesOf("Transcriber", module) .add("Basic", () => ( ( ( , multipleLabels?: boolean, document: string, @@ -22,6 +24,7 @@ export type LabelDocumentProps = { export type SequenceAnnotatorProps = { type: "label-sequence", + hotkeysEnabled?: boolean, labels: Array