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 && (
-
)
+
+ 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