@macrostrat/feedback-components 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +3 -0
- package/dist/edit-state.e8edb13a.js +251 -0
- package/dist/edit-state.e8edb13a.js.map +1 -0
- package/dist/extractions.54be85f8.js +177 -0
- package/dist/extractions.54be85f8.js.map +1 -0
- package/dist/feedback.46c2b5c4.js +252 -0
- package/dist/feedback.46c2b5c4.js.map +1 -0
- package/dist/feedback.module.7e16830e.css +44 -0
- package/dist/feedback.module.7e16830e.css.map +1 -0
- package/dist/feedback.module.c28cbac7.js +28 -0
- package/dist/feedback.module.c28cbac7.js.map +1 -0
- package/dist/graph.cb42b871.js +83 -0
- package/dist/graph.cb42b871.js.map +1 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/main.module.21bbfaf4.js +19 -0
- package/dist/main.module.21bbfaf4.js.map +1 -0
- package/dist/main.module.ca3db294.js +16 -0
- package/dist/main.module.ca3db294.js.map +1 -0
- package/dist/main.module.d6508c0e.css +14 -0
- package/dist/main.module.d6508c0e.css.map +1 -0
- package/dist/main.module.f9f92ece.css +17 -0
- package/dist/main.module.f9f92ece.css.map +1 -0
- package/dist/node.30d0b8c3.js +59 -0
- package/dist/node.30d0b8c3.js.map +1 -0
- package/dist/text-visualizer.77af0d24.js +101 -0
- package/dist/text-visualizer.77af0d24.js.map +1 -0
- package/dist/type-selector.e75dd247.js +62 -0
- package/dist/type-selector.e75dd247.js.map +1 -0
- package/package.json +48 -0
- package/src/extractions/index.ts +219 -0
- package/src/extractions/main.module.sass +10 -0
- package/src/extractions/types.ts +30 -0
- package/src/feedback/edit-state.ts +311 -0
- package/src/feedback/feedback.module.sass +37 -0
- package/src/feedback/graph.ts +98 -0
- package/src/feedback/index.ts +271 -0
- package/src/feedback/node.ts +65 -0
- package/src/feedback/text-visualizer.ts +116 -0
- package/src/feedback/type-selector/index.ts +75 -0
- package/src/feedback/type-selector/main.module.sass +13 -0
- package/src/feedback/types.ts +76 -0
- package/src/index.ts +2 -0
- package/stories/feedback.stories.ts +40 -0
- package/stories/test-data.ts +330 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NodeApi, TreeApi } from "react-arborist";
|
|
2
|
+
import { TreeData } from "./types";
|
|
3
|
+
import { EntityTag } from "../extractions";
|
|
4
|
+
import { useTreeDispatch } from "./edit-state";
|
|
5
|
+
import styles from "./feedback.module.sass";
|
|
6
|
+
import hyper from "@macrostrat/hyper";
|
|
7
|
+
|
|
8
|
+
const h = hyper.styled(styles);
|
|
9
|
+
|
|
10
|
+
function isSelected(searchNode: TreeData, treeNode: TreeData) {
|
|
11
|
+
return searchNode.id == treeNode.id;
|
|
12
|
+
// We could also select children of the search node here if we wanted to
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isNodeHighlighted(node: NodeApi<TreeData>, tree: TreeApi<TreeData>) {
|
|
16
|
+
// We treat no selection as all nodes being active. We may add some nuance later
|
|
17
|
+
if (tree.selectedNodes.length == 0) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const selectedNode of tree.selectedNodes) {
|
|
22
|
+
if (isSelected(node.data, selectedNode.data)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check if the parent node is highlighted
|
|
28
|
+
if (node.parent != null && isNodeHighlighted(node.parent, tree)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNodeActive(node: NodeApi<TreeData>, tree: TreeApi<TreeData>) {
|
|
36
|
+
for (const selectedNode of tree.selectedNodes) {
|
|
37
|
+
if (isSelected(node.data, selectedNode.data)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Node({ node, style, dragHandle, tree, matchComponent }: any) {
|
|
45
|
+
let highlighted: boolean = isNodeHighlighted(node, tree);
|
|
46
|
+
let active: boolean = isNodeActive(node, tree);
|
|
47
|
+
|
|
48
|
+
const dispatch = useTreeDispatch();
|
|
49
|
+
|
|
50
|
+
return h(
|
|
51
|
+
"div.node",
|
|
52
|
+
{ style, ref: dragHandle },
|
|
53
|
+
h(EntityTag, {
|
|
54
|
+
data: node.data,
|
|
55
|
+
active,
|
|
56
|
+
highlighted,
|
|
57
|
+
matchComponent,
|
|
58
|
+
onClickType() {
|
|
59
|
+
dispatch({ type: "toggle-entity-type-selector" });
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default Node;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { AnnotateBlendTag, TextAnnotateBlend } from "react-text-annotate-blend";
|
|
2
|
+
import { InternalEntity } from "./types";
|
|
3
|
+
import { TreeDispatch } from "./edit-state";
|
|
4
|
+
import styles from "./feedback.module.sass";
|
|
5
|
+
import hyper from "@macrostrat/hyper";
|
|
6
|
+
import { buildHighlights, getTagStyle } from "../extractions";
|
|
7
|
+
import { Highlight } from "../extractions/types";
|
|
8
|
+
import { useCallback } from "react";
|
|
9
|
+
|
|
10
|
+
const h = hyper.styled(styles);
|
|
11
|
+
|
|
12
|
+
export interface FeedbackTextProps {
|
|
13
|
+
text: string;
|
|
14
|
+
selectedNodes: number[];
|
|
15
|
+
nodes: InternalEntity[];
|
|
16
|
+
updateNodes: (nodes: string[]) => void;
|
|
17
|
+
dispatch: TreeDispatch;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildTags(
|
|
21
|
+
highlights: Highlight[],
|
|
22
|
+
selectedNodes: number[]
|
|
23
|
+
): AnnotateBlendTag[] {
|
|
24
|
+
let tags: AnnotateBlendTag[] = [];
|
|
25
|
+
|
|
26
|
+
// If entity ID has already been seen, don't add it again
|
|
27
|
+
const entities = new Set<number>();
|
|
28
|
+
|
|
29
|
+
for (const highlight of highlights) {
|
|
30
|
+
// Don't add multiply-linked entities multiple times
|
|
31
|
+
if (entities.has(highlight.id)) continue;
|
|
32
|
+
|
|
33
|
+
const highlighted = isHighlighted(highlight, selectedNodes);
|
|
34
|
+
const active = isActive(highlight, selectedNodes);
|
|
35
|
+
|
|
36
|
+
tags.push({
|
|
37
|
+
markStyle: {
|
|
38
|
+
...getTagStyle(highlight.backgroundColor, {
|
|
39
|
+
highlighted,
|
|
40
|
+
active,
|
|
41
|
+
}),
|
|
42
|
+
borderRadius: "0.2em",
|
|
43
|
+
padding: "0.1em",
|
|
44
|
+
borderWidth: "1.5px",
|
|
45
|
+
cursor: "pointer",
|
|
46
|
+
},
|
|
47
|
+
tagStyle: {
|
|
48
|
+
display: "none",
|
|
49
|
+
},
|
|
50
|
+
...highlight,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
entities.add(highlight.id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return tags;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isActive(tag: Highlight, selectedNodes: number[]) {
|
|
60
|
+
return selectedNodes.includes(tag.id);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isHighlighted(tag: Highlight, selectedNodes: number[]) {
|
|
64
|
+
if (selectedNodes.length === 0) return true;
|
|
65
|
+
return (
|
|
66
|
+
(selectedNodes.includes(tag.id) ||
|
|
67
|
+
tag.parents?.some((d) => selectedNodes.includes(d))) ??
|
|
68
|
+
false
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function FeedbackText(props: FeedbackTextProps) {
|
|
73
|
+
// Convert input to tags
|
|
74
|
+
const { text, selectedNodes, nodes, dispatch } = props;
|
|
75
|
+
let allTags: AnnotateBlendTag[] = buildTags(
|
|
76
|
+
buildHighlights(nodes, null),
|
|
77
|
+
selectedNodes
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const onChange = useCallback(
|
|
81
|
+
(tags) => {
|
|
82
|
+
// New tags
|
|
83
|
+
console.log(tags);
|
|
84
|
+
const newTags = tags.filter((d) => !("id" in d));
|
|
85
|
+
if (newTags.length > 0) {
|
|
86
|
+
const { start, end } = newTags[0];
|
|
87
|
+
const payload = { start, end, text: text.slice(start, end) };
|
|
88
|
+
dispatch({ type: "create-node", payload });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tagIDs = new Set(tags.map((d) => d.id));
|
|
93
|
+
const removedIds = allTags.map((d) => d.id).filter((d) => !tagIDs.has(d));
|
|
94
|
+
|
|
95
|
+
/* Find the id that was removed: that is the one that will be selected
|
|
96
|
+
(we are hijacking the 'click to delete' functionality to select instead) */
|
|
97
|
+
if (removedIds.length > 0) {
|
|
98
|
+
dispatch({
|
|
99
|
+
type: "toggle-node-selected",
|
|
100
|
+
payload: { ids: removedIds },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
[allTags, text]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return h(TextAnnotateBlend, {
|
|
108
|
+
style: {
|
|
109
|
+
fontSize: "1.2em",
|
|
110
|
+
},
|
|
111
|
+
className: "feedback-text",
|
|
112
|
+
content: text,
|
|
113
|
+
onChange,
|
|
114
|
+
value: allTags,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity type selector
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import styles from "./main.module.sass";
|
|
6
|
+
import hyper from "@macrostrat/hyper";
|
|
7
|
+
|
|
8
|
+
import classNames from "classnames";
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { Omnibar, OmnibarProps } from "@blueprintjs/select";
|
|
11
|
+
import "@blueprintjs/select/lib/css/blueprint-select.css";
|
|
12
|
+
|
|
13
|
+
const h = hyper.styled(styles);
|
|
14
|
+
|
|
15
|
+
interface TagItemProps<T> {
|
|
16
|
+
selected: boolean;
|
|
17
|
+
active: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
item: T;
|
|
20
|
+
|
|
21
|
+
onSelect(t: T): void;
|
|
22
|
+
|
|
23
|
+
children?: React.ReactElement;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TagListItem: React.ComponentType<TagItemProps<T>> = (props) => {
|
|
27
|
+
/** Render a tag for the omnibox list */
|
|
28
|
+
let { active, selected, className, onSelect, item, children } = props;
|
|
29
|
+
className = classNames({ active, selected }, className);
|
|
30
|
+
const onClick = () => onSelect(item);
|
|
31
|
+
|
|
32
|
+
return h(
|
|
33
|
+
"div.item-container",
|
|
34
|
+
{
|
|
35
|
+
key: item.id,
|
|
36
|
+
className,
|
|
37
|
+
onClick,
|
|
38
|
+
},
|
|
39
|
+
[
|
|
40
|
+
h("div.swatch", { style: { backgroundColor: item.color } }),
|
|
41
|
+
h("div.item", {}, item.name),
|
|
42
|
+
]
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type BoxLifecycleProps<T> = Pick<OmnibarProps<T>, "onClose" | "isOpen">;
|
|
47
|
+
|
|
48
|
+
interface OmniboxProps<T> extends BoxLifecycleProps<T> {
|
|
49
|
+
items: T[];
|
|
50
|
+
selectedItem: T;
|
|
51
|
+
onSelectItem: (t: T) => void;
|
|
52
|
+
listItemComponent?: React.ComponentType<TagItemProps<T>>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function OmniboxSelector<T>(props: OmniboxProps<T>) {
|
|
56
|
+
/** A general omnibox for annotation types */
|
|
57
|
+
const { onSelectItem, items, isOpen, onClose } = props;
|
|
58
|
+
|
|
59
|
+
return h(Omnibar, {
|
|
60
|
+
onItemSelect: onSelectItem,
|
|
61
|
+
items,
|
|
62
|
+
resetOnSelect: false,
|
|
63
|
+
isOpen,
|
|
64
|
+
onClose,
|
|
65
|
+
itemRenderer(item: T, { handleClick, modifiers }) {
|
|
66
|
+
return h(TagListItem, {
|
|
67
|
+
key: item.id,
|
|
68
|
+
item,
|
|
69
|
+
onSelect: handleClick,
|
|
70
|
+
active: modifiers.active,
|
|
71
|
+
selected: modifiers.active,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { EntityExt } from "../extractions/types";
|
|
2
|
+
|
|
3
|
+
export interface InternalEntity extends EntityExt {
|
|
4
|
+
term_type: string;
|
|
5
|
+
txt_range: number[][];
|
|
6
|
+
children: InternalEntity[];
|
|
7
|
+
orig_id: number;
|
|
8
|
+
id: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TextData {
|
|
12
|
+
preprocessor_id: string;
|
|
13
|
+
extraction_pipeline_id: string;
|
|
14
|
+
model_id: string;
|
|
15
|
+
paper_id: string;
|
|
16
|
+
hashed_text: string;
|
|
17
|
+
weaviate_id: string;
|
|
18
|
+
paragraph_text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Result {
|
|
22
|
+
text: TextData;
|
|
23
|
+
entities?: InternalEntity[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type TreeData = EntityExt;
|
|
27
|
+
|
|
28
|
+
export interface ServerRelationship {
|
|
29
|
+
src_name: string;
|
|
30
|
+
dst_name: string;
|
|
31
|
+
relationship_type: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ServerEntity {
|
|
35
|
+
entity_name: string;
|
|
36
|
+
entity_type: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ServerResponse {
|
|
40
|
+
text: TextData;
|
|
41
|
+
relationships: ServerRelationship[];
|
|
42
|
+
just_entities: ServerEntity[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RunText {
|
|
46
|
+
preprocessor_id: string;
|
|
47
|
+
paper_id: string;
|
|
48
|
+
hashed_text: string;
|
|
49
|
+
weaviate_id: string;
|
|
50
|
+
paragraph_text: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RunRelationship {
|
|
54
|
+
src: string;
|
|
55
|
+
dst: string;
|
|
56
|
+
relationship_type: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RunEntity {
|
|
60
|
+
entity: string;
|
|
61
|
+
entity_type: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RunSource {
|
|
65
|
+
text: RunText;
|
|
66
|
+
relationships: RunRelationship[];
|
|
67
|
+
just_entities: RunEntity[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RunRecord {
|
|
71
|
+
run_id: string;
|
|
72
|
+
extraction_pipeline_id: string;
|
|
73
|
+
user_name: string;
|
|
74
|
+
model_id: string;
|
|
75
|
+
results: RunSource[];
|
|
76
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
|
|
3
|
+
import { FeedbackComponent } from "../src";
|
|
4
|
+
import { data, entityTypes } from "./test-data";
|
|
5
|
+
import h from "@macrostrat/hyper";
|
|
6
|
+
|
|
7
|
+
function FeedbackInterface({ data, types }) {
|
|
8
|
+
const { entities = [], paragraph_text, model, model_run, source_text } = data;
|
|
9
|
+
|
|
10
|
+
return h(FeedbackComponent, {
|
|
11
|
+
entities,
|
|
12
|
+
text: paragraph_text,
|
|
13
|
+
model,
|
|
14
|
+
entityTypes: createMap(types),
|
|
15
|
+
sourceTextID: source_text,
|
|
16
|
+
runID: model_run,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
|
20
|
+
const meta: Meta<any> = {
|
|
21
|
+
title: "Text extractions/Text extraction",
|
|
22
|
+
component: FeedbackInterface,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
|
|
27
|
+
export const Primary: StoryObj<{}> = {
|
|
28
|
+
args: {
|
|
29
|
+
data,
|
|
30
|
+
types: entityTypes,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function createMap(arr) {
|
|
35
|
+
const out = new Map();
|
|
36
|
+
for (const d of arr) {
|
|
37
|
+
out.set(d.id, d);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|