@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,311 @@
|
|
|
1
|
+
import { TreeData } from "./types";
|
|
2
|
+
import { createContext, Dispatch, useContext, useReducer } from "react";
|
|
3
|
+
import update, { Spec } from "immutability-helper";
|
|
4
|
+
import { EntityType } from "../extractions/types";
|
|
5
|
+
|
|
6
|
+
export enum ViewMode {
|
|
7
|
+
Tree = "tree",
|
|
8
|
+
Graph = "graph",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface TreeState {
|
|
12
|
+
initialTree: TreeData[];
|
|
13
|
+
tree: TreeData[];
|
|
14
|
+
selectedNodes: number[];
|
|
15
|
+
entityTypesMap: Map<number, EntityType>;
|
|
16
|
+
selectedEntityType: EntityType;
|
|
17
|
+
lastInternalId: number;
|
|
18
|
+
isSelectingEntityType: boolean;
|
|
19
|
+
viewMode: ViewMode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type TextRange = {
|
|
23
|
+
start: number;
|
|
24
|
+
end: number;
|
|
25
|
+
text: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type TreeAction =
|
|
29
|
+
| {
|
|
30
|
+
type: "move-node";
|
|
31
|
+
payload: { dragIds: number[]; parentId: number; index: number };
|
|
32
|
+
}
|
|
33
|
+
| { type: "delete-node"; payload: { ids: number[] } }
|
|
34
|
+
| { type: "select-node"; payload: { ids: number[] } }
|
|
35
|
+
| { type: "toggle-node-selected"; payload: { ids: number[] } }
|
|
36
|
+
| { type: "set-view-mode"; payload: ViewMode }
|
|
37
|
+
| { type: "create-node"; payload: TextRange }
|
|
38
|
+
| { type: "select-entity-type"; payload: EntityType }
|
|
39
|
+
| { type: "toggle-entity-type-selector"; payload?: boolean | null }
|
|
40
|
+
| { type: "deselect" }
|
|
41
|
+
| { type: "reset" };
|
|
42
|
+
|
|
43
|
+
export type TreeDispatch = Dispatch<TreeAction>;
|
|
44
|
+
|
|
45
|
+
export function useUpdatableTree(
|
|
46
|
+
initialTree: TreeData[],
|
|
47
|
+
entityTypes: Map<number, EntityType>
|
|
48
|
+
): [TreeState, TreeDispatch] {
|
|
49
|
+
// Get the first entity type
|
|
50
|
+
const type = entityTypes.values().next().value;
|
|
51
|
+
|
|
52
|
+
return useReducer(treeReducer, {
|
|
53
|
+
initialTree,
|
|
54
|
+
tree: initialTree,
|
|
55
|
+
selectedNodes: [],
|
|
56
|
+
entityTypesMap: entityTypes,
|
|
57
|
+
selectedEntityType: type,
|
|
58
|
+
lastInternalId: 0,
|
|
59
|
+
isSelectingEntityType: false,
|
|
60
|
+
viewMode: ViewMode.Tree,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const TreeDispatchContext = createContext<TreeDispatch | null>(null);
|
|
65
|
+
|
|
66
|
+
export function useTreeDispatch() {
|
|
67
|
+
const dispatch = useContext(TreeDispatchContext);
|
|
68
|
+
if (dispatch == null) {
|
|
69
|
+
throw new Error("No dispatch context available");
|
|
70
|
+
}
|
|
71
|
+
return dispatch;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function treeReducer(state: TreeState, action: TreeAction) {
|
|
75
|
+
console.log(action);
|
|
76
|
+
switch (action.type) {
|
|
77
|
+
case "move-node":
|
|
78
|
+
// For each node in the tree, if the node is in the dragIds, remove it from the tree and collect it
|
|
79
|
+
const [newTree, removedNodes] = removeNodes(
|
|
80
|
+
state.tree,
|
|
81
|
+
action.payload.dragIds
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
let keyPath: (number | "children")[] = [];
|
|
85
|
+
if (action.payload.parentId) {
|
|
86
|
+
keyPath = findNode(newTree, action.payload.parentId);
|
|
87
|
+
keyPath.push("children");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Add removed nodes to the new tree at the correct location
|
|
91
|
+
let updateSpec = buildNestedSpec(keyPath, {
|
|
92
|
+
$splice: [[action.payload.index, 0, ...removedNodes]],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { ...state, tree: update(newTree, updateSpec) };
|
|
96
|
+
case "delete-node":
|
|
97
|
+
// For each node in the tree, if the node is in the ids, remove it from the tree
|
|
98
|
+
const [newTree2, _removedNodes] = removeNodes(
|
|
99
|
+
state.tree,
|
|
100
|
+
action.payload.ids
|
|
101
|
+
);
|
|
102
|
+
// Get children of the removed nodes
|
|
103
|
+
// If children are not present elsewhere in the tree, insert them
|
|
104
|
+
|
|
105
|
+
const children = _removedNodes
|
|
106
|
+
.flatMap((node) => node.children ?? [])
|
|
107
|
+
.filter((child) => !nodeIsInTree(newTree2, child.id));
|
|
108
|
+
|
|
109
|
+
// Reset the selection
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...state,
|
|
113
|
+
tree: [...newTree2, ...children],
|
|
114
|
+
selectedNodes: state.selectedNodes.filter(
|
|
115
|
+
(id) => !action.payload.ids.includes(id)
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
case "select-node":
|
|
119
|
+
const { ids } = action.payload;
|
|
120
|
+
return { ...state, selectedNodes: ids };
|
|
121
|
+
// otherwise fall through to toggle-node-selected for a single ID
|
|
122
|
+
case "toggle-node-selected":
|
|
123
|
+
const nodesToAdd = action.payload.ids.filter(
|
|
124
|
+
(id) => !state.selectedNodes.includes(id)
|
|
125
|
+
);
|
|
126
|
+
const nodesToKeep = state.selectedNodes.filter(
|
|
127
|
+
(id) => !action.payload.ids.includes(id)
|
|
128
|
+
);
|
|
129
|
+
return { ...state, selectedNodes: [...nodesToKeep, ...nodesToAdd] };
|
|
130
|
+
|
|
131
|
+
case "create-node":
|
|
132
|
+
const newId = state.lastInternalId - 1;
|
|
133
|
+
const { text, start, end } = action.payload;
|
|
134
|
+
const node: TreeData = {
|
|
135
|
+
id: newId,
|
|
136
|
+
name: text,
|
|
137
|
+
children: [],
|
|
138
|
+
indices: [start, end],
|
|
139
|
+
type: state.selectedEntityType,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
...state,
|
|
144
|
+
tree: [...state.tree, node],
|
|
145
|
+
selectedNodes: [newId],
|
|
146
|
+
lastInternalId: newId,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** Entity type selection */
|
|
150
|
+
case "toggle-entity-type-selector":
|
|
151
|
+
return {
|
|
152
|
+
...state,
|
|
153
|
+
isSelectingEntityType: action.payload ?? !state.isSelectingEntityType,
|
|
154
|
+
};
|
|
155
|
+
case "select-entity-type": {
|
|
156
|
+
// For each selected node, update the type
|
|
157
|
+
let newTree2 = state.tree;
|
|
158
|
+
for (let id of state.selectedNodes) {
|
|
159
|
+
const keyPath = findNode(state.tree, id);
|
|
160
|
+
const nestedSpec = buildNestedSpec(keyPath, {
|
|
161
|
+
type: { $set: action.payload },
|
|
162
|
+
});
|
|
163
|
+
newTree2 = update(newTree2, nestedSpec);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
...state,
|
|
168
|
+
tree: newTree2,
|
|
169
|
+
selectedEntityType: action.payload,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
case "deselect":
|
|
173
|
+
return { ...state, selectedNodes: [] };
|
|
174
|
+
case "reset":
|
|
175
|
+
return {
|
|
176
|
+
...state,
|
|
177
|
+
tree: state.initialTree,
|
|
178
|
+
selectedNodes: [],
|
|
179
|
+
};
|
|
180
|
+
case "set-view-mode":
|
|
181
|
+
return { ...state, viewMode: action.payload };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function nodeIsInTree(tree: TreeData[], id: number): boolean {
|
|
186
|
+
for (let node of tree) {
|
|
187
|
+
if (node.id == id) {
|
|
188
|
+
return true;
|
|
189
|
+
} else if (node.children) {
|
|
190
|
+
if (nodeIsInTree(node.children, id)) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildNestedSpec(
|
|
199
|
+
keyPath: (number | "children")[],
|
|
200
|
+
innerSpec: Spec<any>
|
|
201
|
+
): Spec<TreeData[]> {
|
|
202
|
+
// Build a nested object from a key path
|
|
203
|
+
|
|
204
|
+
let spec = innerSpec;
|
|
205
|
+
for (let i = keyPath.length - 1; i >= 0; i--) {
|
|
206
|
+
spec = { [keyPath[i]]: spec };
|
|
207
|
+
}
|
|
208
|
+
return spec as any;
|
|
209
|
+
// Since we don't have a "children" key at the root, we make the top-level spec an array
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function findNode(
|
|
213
|
+
tree: TreeData[],
|
|
214
|
+
id: number
|
|
215
|
+
): (number | "children")[] | null {
|
|
216
|
+
// Find the index of the node with the given id in the tree, returning the key path
|
|
217
|
+
for (let i = 0; i < tree.length; i++) {
|
|
218
|
+
if (tree[i].id == id) {
|
|
219
|
+
return [i];
|
|
220
|
+
} else if (tree[i].children) {
|
|
221
|
+
let path = findNode(tree[i].children, id);
|
|
222
|
+
if (path != null) {
|
|
223
|
+
return [i, "children", ...path];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function removeNodes(
|
|
231
|
+
tree: TreeData[],
|
|
232
|
+
ids: number[]
|
|
233
|
+
): [TreeData[], TreeData[]] {
|
|
234
|
+
/** Remove nodes with the given ids from the tree and return the new tree and the removed nodes */
|
|
235
|
+
let newTree: TreeData[] = [];
|
|
236
|
+
let removedNodes: TreeData[] = [];
|
|
237
|
+
|
|
238
|
+
for (let node of tree) {
|
|
239
|
+
if (ids.includes(node.id)) {
|
|
240
|
+
removedNodes.push(node);
|
|
241
|
+
} else {
|
|
242
|
+
// Recurse into children
|
|
243
|
+
if (node.children) {
|
|
244
|
+
let [newChildren, removedChildren] = removeNodes(node.children, ids);
|
|
245
|
+
node = { ...node, children: newChildren };
|
|
246
|
+
removedNodes.push(...removedChildren);
|
|
247
|
+
}
|
|
248
|
+
newTree.push(node);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return [newTree, removedNodes];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface EntityOutput {
|
|
256
|
+
id: number;
|
|
257
|
+
type: number | null;
|
|
258
|
+
txt_range: number[][];
|
|
259
|
+
name: string;
|
|
260
|
+
match: any | null;
|
|
261
|
+
reasoning: string | null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface GraphData {
|
|
265
|
+
nodes: EntityOutput[];
|
|
266
|
+
edges: { source: number; dest: number }[];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function treeToGraph(tree: TreeData[]): GraphData {
|
|
270
|
+
// Convert the tree to a graph
|
|
271
|
+
let nodes: EntityOutput[] = [];
|
|
272
|
+
let edges: { source: number; dest: number }[] = [];
|
|
273
|
+
const nodeMap = new Map<number, TreeData>();
|
|
274
|
+
|
|
275
|
+
for (let node of tree) {
|
|
276
|
+
// If we've already found an instance of this node, we don't need to record
|
|
277
|
+
// it again
|
|
278
|
+
if (nodeMap.has(node.id)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const { indices, id, name } = node;
|
|
283
|
+
|
|
284
|
+
const nodeData: EntityOutput = {
|
|
285
|
+
id,
|
|
286
|
+
type: node.type.id,
|
|
287
|
+
name,
|
|
288
|
+
txt_range: [indices],
|
|
289
|
+
reasoning: null,
|
|
290
|
+
match: node.match,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
nodeMap.set(node.id, node);
|
|
294
|
+
nodes.push(nodeData);
|
|
295
|
+
|
|
296
|
+
if (node.children) {
|
|
297
|
+
for (let child of node.children) {
|
|
298
|
+
edges.push({ source: node.id, dest: child.id });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Now process the children
|
|
302
|
+
const { nodes: childNodes, edges: childEdges } = treeToGraph(
|
|
303
|
+
node.children
|
|
304
|
+
);
|
|
305
|
+
nodes.push(...childNodes);
|
|
306
|
+
edges.push(...childEdges);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { nodes, edges };
|
|
311
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.feedback-component
|
|
2
|
+
position: relative
|
|
3
|
+
width: 800px
|
|
4
|
+
|
|
5
|
+
& > svg
|
|
6
|
+
width: 800px
|
|
7
|
+
|
|
8
|
+
.node
|
|
9
|
+
cursor: pointer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
.feedback-text
|
|
13
|
+
margin-bottom: 2em
|
|
14
|
+
|
|
15
|
+
.entity-panel
|
|
16
|
+
position: relative
|
|
17
|
+
max-height: 600px
|
|
18
|
+
|
|
19
|
+
.control-panel
|
|
20
|
+
max-width: 15em
|
|
21
|
+
position: absolute
|
|
22
|
+
top: 1em
|
|
23
|
+
right: 1em
|
|
24
|
+
padding: 0.2em 0.5em
|
|
25
|
+
|
|
26
|
+
.entity-panel
|
|
27
|
+
flex: 1
|
|
28
|
+
min-height: 100px
|
|
29
|
+
padding: 1em
|
|
30
|
+
background: var(--panel-secondary-background-color)
|
|
31
|
+
border-radius: 4px
|
|
32
|
+
// Inset box shadow
|
|
33
|
+
box-shadow: 0 0 0 1px var(--panel-border-color) inset
|
|
34
|
+
|
|
35
|
+
.selection-tree
|
|
36
|
+
margin: -1em 0
|
|
37
|
+
padding: 1em 0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { TreeData } from "./types";
|
|
2
|
+
import { treeToGraph } from "./edit-state";
|
|
3
|
+
import h from "@macrostrat/hyper";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
forceSimulation,
|
|
7
|
+
SimulationNodeDatum,
|
|
8
|
+
SimulationLinkDatum,
|
|
9
|
+
forceCenter,
|
|
10
|
+
forceLink,
|
|
11
|
+
forceManyBody,
|
|
12
|
+
forceCollide,
|
|
13
|
+
} from "d3-force";
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { Spinner } from "@blueprintjs/core";
|
|
16
|
+
|
|
17
|
+
export function GraphView(props: {
|
|
18
|
+
tree: TreeData[];
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}) {
|
|
22
|
+
// A graph view with react-flow
|
|
23
|
+
// Get positions of nodes using force simulation
|
|
24
|
+
const { tree, width, height } = props;
|
|
25
|
+
|
|
26
|
+
const [nodes, setNodes] = useState<SimulationNodeDatum[]>(null);
|
|
27
|
+
const [links, setLinks] = useState<SimulationLinkDatum[]>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const { nodes, edges } = treeToGraph(tree);
|
|
31
|
+
|
|
32
|
+
const nodesMap = new Map<number, SimulationNodeDatum>(
|
|
33
|
+
nodes.map((d) => [d.id, d])
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const links = edges.map((d) => {
|
|
37
|
+
return {
|
|
38
|
+
source: nodesMap.get(d.source),
|
|
39
|
+
target: nodesMap.get(d.dest),
|
|
40
|
+
strength: 1,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const simulation = forceSimulation(nodes)
|
|
45
|
+
.force("link", forceLink(links))
|
|
46
|
+
.force("charge", forceManyBody().strength(-50))
|
|
47
|
+
.force("center", forceCenter(width / 2, height / 2))
|
|
48
|
+
.force("collide", forceCollide().radius(20))
|
|
49
|
+
.on("tick", () => {
|
|
50
|
+
// Update the positions of the nodes
|
|
51
|
+
// setNodes(nodes);
|
|
52
|
+
console.log("Simulation tick");
|
|
53
|
+
})
|
|
54
|
+
.on("end", () => {
|
|
55
|
+
// Update the positions of the nodes
|
|
56
|
+
setNodes(nodes);
|
|
57
|
+
setLinks(links);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
simulation.stop();
|
|
62
|
+
};
|
|
63
|
+
}, [tree, width, height]);
|
|
64
|
+
|
|
65
|
+
if (nodes == null || links == null) {
|
|
66
|
+
return h(Spinner);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log("Graph", nodes, links);
|
|
70
|
+
|
|
71
|
+
return h("div.graph-view", { style: { width, height } }, [
|
|
72
|
+
h("svg", { width, height }, [
|
|
73
|
+
h(
|
|
74
|
+
"g.nodes",
|
|
75
|
+
nodes.map((d) => {
|
|
76
|
+
return h("circle", {
|
|
77
|
+
cx: d.x,
|
|
78
|
+
cy: d.y,
|
|
79
|
+
r: 5,
|
|
80
|
+
fill: "blue",
|
|
81
|
+
});
|
|
82
|
+
})
|
|
83
|
+
),
|
|
84
|
+
h(
|
|
85
|
+
"g.links",
|
|
86
|
+
links.map((d) => {
|
|
87
|
+
return h("line", {
|
|
88
|
+
x1: d.source.x,
|
|
89
|
+
y1: d.source.y,
|
|
90
|
+
x2: d.target.x,
|
|
91
|
+
y2: d.target.y,
|
|
92
|
+
stroke: "black",
|
|
93
|
+
});
|
|
94
|
+
})
|
|
95
|
+
),
|
|
96
|
+
]),
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import styles from "./feedback.module.sass";
|
|
2
|
+
import hyper from "@macrostrat/hyper";
|
|
3
|
+
|
|
4
|
+
import { Tree, TreeApi } from "react-arborist";
|
|
5
|
+
import Node from "./node";
|
|
6
|
+
import { FeedbackText } from "./text-visualizer";
|
|
7
|
+
import type { InternalEntity, TreeData } from "./types";
|
|
8
|
+
import type { Entity } from "../extractions";
|
|
9
|
+
import { ModelInfo } from "../extractions";
|
|
10
|
+
import {
|
|
11
|
+
TreeDispatchContext,
|
|
12
|
+
treeToGraph,
|
|
13
|
+
useUpdatableTree,
|
|
14
|
+
ViewMode,
|
|
15
|
+
} from "./edit-state";
|
|
16
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
17
|
+
import { ButtonGroup, Card, SegmentedControl } from "@blueprintjs/core";
|
|
18
|
+
import { OmniboxSelector } from "./type-selector";
|
|
19
|
+
import {
|
|
20
|
+
CancelButton,
|
|
21
|
+
DataField,
|
|
22
|
+
FlexBox,
|
|
23
|
+
FlexRow,
|
|
24
|
+
SaveButton,
|
|
25
|
+
} from "@macrostrat/ui-components";
|
|
26
|
+
import useElementDimensions from "use-element-dimensions";
|
|
27
|
+
import { GraphView } from "./graph";
|
|
28
|
+
|
|
29
|
+
export type { GraphData } from "./edit-state";
|
|
30
|
+
export { treeToGraph } from "./edit-state";
|
|
31
|
+
export type { TreeData } from "./types";
|
|
32
|
+
|
|
33
|
+
const h = hyper.styled(styles);
|
|
34
|
+
|
|
35
|
+
function setsAreTheSame<T>(a: Set<T>, b: Set<T>) {
|
|
36
|
+
if (a.size !== b.size) return false;
|
|
37
|
+
for (const item of a) {
|
|
38
|
+
if (!b.has(item)) return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function FeedbackComponent({
|
|
44
|
+
entities = [],
|
|
45
|
+
text,
|
|
46
|
+
model,
|
|
47
|
+
entityTypes,
|
|
48
|
+
matchComponent,
|
|
49
|
+
onSave,
|
|
50
|
+
}) {
|
|
51
|
+
// Get the input arguments
|
|
52
|
+
|
|
53
|
+
const [state, dispatch] = useUpdatableTree(
|
|
54
|
+
entities.map(processEntity) as any,
|
|
55
|
+
entityTypes
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const { selectedNodes, tree, selectedEntityType, isSelectingEntityType } =
|
|
59
|
+
state;
|
|
60
|
+
|
|
61
|
+
const [{ width, height }, ref] = useElementDimensions();
|
|
62
|
+
|
|
63
|
+
return h(TreeDispatchContext.Provider, { value: dispatch }, [
|
|
64
|
+
h(FeedbackText, {
|
|
65
|
+
text,
|
|
66
|
+
dispatch,
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
nodes: tree,
|
|
69
|
+
selectedNodes,
|
|
70
|
+
}),
|
|
71
|
+
h(FlexRow, { alignItems: "baseline", justifyContent: "space-between" }, [
|
|
72
|
+
h(ModelInfo, { data: model }),
|
|
73
|
+
h(SegmentedControl, {
|
|
74
|
+
options: [
|
|
75
|
+
{ label: "Tree", value: "tree" },
|
|
76
|
+
{ label: "Graph", value: "graph" },
|
|
77
|
+
],
|
|
78
|
+
value: state.viewMode,
|
|
79
|
+
small: true,
|
|
80
|
+
onValueChange(value: ViewMode) {
|
|
81
|
+
console.log("Setting view mode", value);
|
|
82
|
+
dispatch({ type: "set-view-mode", payload: value });
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
]),
|
|
86
|
+
h(
|
|
87
|
+
"div.entity-panel",
|
|
88
|
+
{
|
|
89
|
+
ref,
|
|
90
|
+
},
|
|
91
|
+
[
|
|
92
|
+
h(Card, { className: "control-panel" }, [
|
|
93
|
+
h(
|
|
94
|
+
ButtonGroup,
|
|
95
|
+
{
|
|
96
|
+
vertical: true,
|
|
97
|
+
fill: true,
|
|
98
|
+
minimal: true,
|
|
99
|
+
alignText: "left",
|
|
100
|
+
},
|
|
101
|
+
[
|
|
102
|
+
h(
|
|
103
|
+
CancelButton,
|
|
104
|
+
{
|
|
105
|
+
icon: "trash",
|
|
106
|
+
disabled: state.initialTree == state.tree,
|
|
107
|
+
onClick() {
|
|
108
|
+
dispatch({ type: "reset" });
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
"Reset"
|
|
112
|
+
),
|
|
113
|
+
h(
|
|
114
|
+
SaveButton,
|
|
115
|
+
{
|
|
116
|
+
onClick() {
|
|
117
|
+
onSave(state.tree);
|
|
118
|
+
},
|
|
119
|
+
disabled: state.initialTree == state.tree,
|
|
120
|
+
},
|
|
121
|
+
"Save"
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
),
|
|
125
|
+
h(EntityTypeSelector, {
|
|
126
|
+
entityTypes,
|
|
127
|
+
selected: selectedEntityType,
|
|
128
|
+
onChange(payload) {
|
|
129
|
+
dispatch({ type: "select-entity-type", payload });
|
|
130
|
+
},
|
|
131
|
+
isOpen: isSelectingEntityType,
|
|
132
|
+
setOpen: (isOpen: boolean) =>
|
|
133
|
+
dispatch({
|
|
134
|
+
type: "toggle-entity-type-selector",
|
|
135
|
+
payload: isOpen,
|
|
136
|
+
}),
|
|
137
|
+
}),
|
|
138
|
+
]),
|
|
139
|
+
h.if(state.viewMode == "tree")(ManagedSelectionTree, {
|
|
140
|
+
selectedNodes,
|
|
141
|
+
dispatch,
|
|
142
|
+
tree,
|
|
143
|
+
width,
|
|
144
|
+
height,
|
|
145
|
+
matchComponent,
|
|
146
|
+
}),
|
|
147
|
+
h.if(state.viewMode == "graph")(GraphView, {
|
|
148
|
+
tree,
|
|
149
|
+
width,
|
|
150
|
+
height,
|
|
151
|
+
}),
|
|
152
|
+
]
|
|
153
|
+
),
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function processEntity(entity: Entity): InternalEntity {
|
|
158
|
+
// @ts-ignore
|
|
159
|
+
return {
|
|
160
|
+
...entity,
|
|
161
|
+
// @ts-ignore
|
|
162
|
+
term_type: entity.type.name,
|
|
163
|
+
txt_range: [entity.indices],
|
|
164
|
+
children: entity.children?.map(processEntity) ?? [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function EntityTypeSelector({
|
|
169
|
+
entityTypes,
|
|
170
|
+
selected,
|
|
171
|
+
isOpen,
|
|
172
|
+
setOpen,
|
|
173
|
+
onChange,
|
|
174
|
+
}) {
|
|
175
|
+
// Show all entity types when selected is null
|
|
176
|
+
const _selected = selected != null ? selected : undefined;
|
|
177
|
+
return h(DataField, { label: "Entity type", inline: true }, [
|
|
178
|
+
h(
|
|
179
|
+
"code.bp5-code",
|
|
180
|
+
{
|
|
181
|
+
onClick() {
|
|
182
|
+
setOpen((d) => !d);
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
selected.name
|
|
186
|
+
),
|
|
187
|
+
h(OmniboxSelector, {
|
|
188
|
+
isOpen,
|
|
189
|
+
items: Array.from(entityTypes.values()),
|
|
190
|
+
selectedItem: _selected,
|
|
191
|
+
onSelectItem(item) {
|
|
192
|
+
setOpen(false);
|
|
193
|
+
onChange(item);
|
|
194
|
+
},
|
|
195
|
+
onClose() {
|
|
196
|
+
setOpen(false);
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function ManagedSelectionTree(props) {
|
|
203
|
+
const {
|
|
204
|
+
selectedNodes,
|
|
205
|
+
dispatch,
|
|
206
|
+
tree,
|
|
207
|
+
height,
|
|
208
|
+
width,
|
|
209
|
+
matchComponent,
|
|
210
|
+
...rest
|
|
211
|
+
} = props;
|
|
212
|
+
|
|
213
|
+
const ref = useRef<TreeApi<TreeData>>();
|
|
214
|
+
|
|
215
|
+
const _Node = useCallback(
|
|
216
|
+
(props) => h(Node, { ...props, matchComponent }),
|
|
217
|
+
[matchComponent]
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (ref.current == null) return;
|
|
222
|
+
// Check if selection matches current
|
|
223
|
+
const selection = new Set(selectedNodes.map((d) => d.toString()));
|
|
224
|
+
const currentSelection = ref.current.selectedIds;
|
|
225
|
+
if (setsAreTheSame(selection, currentSelection)) return;
|
|
226
|
+
// If the selection is the same, do nothing
|
|
227
|
+
|
|
228
|
+
// Set selection
|
|
229
|
+
ref.current.setSelection({
|
|
230
|
+
ids: selectedNodes.map((d) => d.toString()),
|
|
231
|
+
anchor: null,
|
|
232
|
+
mostRecent: null,
|
|
233
|
+
});
|
|
234
|
+
}, [selectedNodes]);
|
|
235
|
+
|
|
236
|
+
return h(Tree, {
|
|
237
|
+
className: "selection-tree",
|
|
238
|
+
height,
|
|
239
|
+
width,
|
|
240
|
+
ref,
|
|
241
|
+
data: tree,
|
|
242
|
+
onMove({ dragIds, parentId, index }) {
|
|
243
|
+
dispatch({
|
|
244
|
+
type: "move-node",
|
|
245
|
+
payload: {
|
|
246
|
+
dragIds: dragIds.map((d) => parseInt(d)),
|
|
247
|
+
parentId: parentId ? parseInt(parentId) : null,
|
|
248
|
+
index,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
onDelete({ ids }) {
|
|
253
|
+
dispatch({
|
|
254
|
+
type: "delete-node",
|
|
255
|
+
payload: { ids: ids.map((d) => parseInt(d)) },
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
onSelect(nodes) {
|
|
259
|
+
let ids = nodes.map((d) => parseInt(d.id));
|
|
260
|
+
if (ids.length == 1 && ids[0] == selectedNodes[0]) {
|
|
261
|
+
// Deselect
|
|
262
|
+
ids = [];
|
|
263
|
+
}
|
|
264
|
+
dispatch({ type: "select-node", payload: { ids } });
|
|
265
|
+
},
|
|
266
|
+
children: _Node,
|
|
267
|
+
idAccessor(d: TreeData) {
|
|
268
|
+
return d.id.toString();
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|