From 3a11ddce83c112edaf90b6bc311eef1a9e69f7ab Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Mon, 14 Oct 2024 13:37:42 -0500 Subject: [PATCH 1/6] Fixed editing capabilities --- pages/integrations/xdd/extractions/lib/index.ts | 4 ++-- .../xdd/feedback/@sourceTextID/+Page.client.ts | 7 +------ .../xdd/feedback/@sourceTextID/lib/edit-state.ts | 9 ++++++++- .../integrations/xdd/feedback/@sourceTextID/lib/index.ts | 4 +++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pages/integrations/xdd/extractions/lib/index.ts b/pages/integrations/xdd/extractions/lib/index.ts index 80652661..0617fcd9 100644 --- a/pages/integrations/xdd/extractions/lib/index.ts +++ b/pages/integrations/xdd/extractions/lib/index.ts @@ -114,9 +114,9 @@ export function EntityTag({ data, selected = true }) { const style = getTagStyle(type.color, { selected }); return h(Tag, { style, className }, [ - h("code.entity-type.bp5-code", type.name), - " ", h("span.entity-name", name), + " ", + h("code.entity-type.bp5-code", type.name), h(Match, { data: match }), ]); } diff --git a/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts index 1bb8f026..f61368af 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts @@ -12,12 +12,7 @@ import { FeedbackComponent } from "./lib"; import { JSONView } from "@macrostrat/ui-components"; import { create } from "zustand"; import { useEffect } from "react"; -import { - Card, - NonIdealState, - OverlaysProvider, - Spinner, -} from "@blueprintjs/core"; +import { NonIdealState, OverlaysProvider, Spinner } from "@blueprintjs/core"; /** * Get a single text window for feedback purposes diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts index 500d600b..0ef08905 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts @@ -27,7 +27,8 @@ type TreeAction = | { type: "select-node"; payload: { ids: number[] } } | { type: "toggle-node-selected"; payload: { ids: number[] } } | { type: "create-node"; payload: TextRange } - | { type: "select-entity-type"; payload: EntityType }; + | { type: "select-entity-type"; payload: EntityType } + | { type: "reset" }; export type TreeDispatch = Dispatch; @@ -143,6 +144,12 @@ function treeReducer(state: TreeState, action: TreeAction) { selectedEntityType: action.payload, }; } + case "reset": + return { + ...state, + tree: state.initialTree, + selectedNodes: [], + }; } } diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts index 0da6a82a..dc367f79 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts @@ -74,6 +74,8 @@ function processEntity(entity: Entity): InternalEntity { function EntityTypeSelector({ entityTypes, selected, onChange }) { const [isOpen, setOpen] = useState(false); + // Show all entity types when selected is null + const _selected = selected != null ? selected : undefined; return h(DataField, { label: "Entity type", inline: true }, [ h( "code.bp5-code", @@ -87,7 +89,7 @@ function EntityTypeSelector({ entityTypes, selected, onChange }) { h(OmniboxSelector, { isOpen, items: Array.from(entityTypes.values()), - selectedItem: selected, + selectedItem: _selected, onSelectItem(item) { setOpen(false); onChange(item); From 5a2de461006a3f12eeb9e8852f27479cfb06089c Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Mon, 14 Oct 2024 14:04:50 -0500 Subject: [PATCH 2/6] Starting to separate active vs. highlighted tags --- .../integrations/xdd/extractions/lib/index.ts | 20 +++++++++++------ .../feedback/@sourceTextID/lib/edit-state.ts | 3 --- .../xdd/feedback/@sourceTextID/lib/node.ts | 22 ++++++++++++++++--- .../@sourceTextID/lib/text-visualizer.ts | 7 +++--- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/pages/integrations/xdd/extractions/lib/index.ts b/pages/integrations/xdd/extractions/lib/index.ts index 0617fcd9..d51ad153 100644 --- a/pages/integrations/xdd/extractions/lib/index.ts +++ b/pages/integrations/xdd/extractions/lib/index.ts @@ -34,18 +34,23 @@ export function enhanceData(extractionData, models, entityTypes) { export function getTagStyle( baseColor: string, - options: { selected?: boolean; inDarkMode?: boolean } + options: { highlighted?: boolean; inDarkMode?: boolean; active?: boolean } ): CSSProperties { const _baseColor = asChromaColor(baseColor ?? "#ddd"); - const { selected = true, inDarkMode = false } = options; + const { highlighted = true, inDarkMode = false, active = false } = options; - const mixAmount = selected ? 0.8 : 0.5; - const backgroundAlpha = selected ? 0.8 : 0.2; + let mixAmount = highlighted ? 0.8 : 0.5; + let backgroundAlpha = highlighted ? 0.8 : 0.2; + + if (active) { + mixAmount = 1; + backgroundAlpha = 1; + } const mixTarget = inDarkMode ? "white" : "black"; const color = _baseColor.mix(mixTarget, mixAmount).css(); - const borderColor = selected + const borderColor = highlighted ? _baseColor.mix(mixTarget, mixAmount / 2).css() : "transparent"; @@ -56,6 +61,7 @@ export function getTagStyle( borderStyle: "solid", borderColor, borderWidth: "1px", + fontWeight: active ? "bold" : "normal", }; } @@ -101,7 +107,7 @@ export function ModelInfo({ data }) { return h("p.model-name", ["Model: ", h("code.bp5-code", data.name)]); } -export function EntityTag({ data, selected = true }) { +export function EntityTag({ data, highlighted = true, active = false }) { const { name, type, match } = data; const className = classNames( { @@ -111,7 +117,7 @@ export function EntityTag({ data, selected = true }) { "entity" ); - const style = getTagStyle(type.color, { selected }); + const style = getTagStyle(type.color, { highlighted, active }); return h(Tag, { style, className }, [ h("span.entity-name", name), diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts index 0ef08905..b1c9ba7c 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts @@ -110,7 +110,6 @@ function treeReducer(state: TreeState, action: TreeAction) { case "create-node": const newId = state.lastInternalId - 1; const { text, start, end } = action.payload; - console.log(action.payload); const node: TreeData = { id: newId, name: text, @@ -130,11 +129,9 @@ function treeReducer(state: TreeState, action: TreeAction) { let newTree2 = state.tree; for (let id of state.selectedNodes) { const keyPath = findNode(state.tree, id); - console.log(keyPath); const nestedSpec = buildNestedSpec(keyPath, { type: { $set: action.payload }, }); - console.log(nestedSpec); newTree2 = update(newTree2, nestedSpec); } diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/node.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/node.ts index e2bfb203..c8f6680a 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/node.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/node.ts @@ -8,27 +8,43 @@ function isSelected(searchNode: TreeData, treeNode: TreeData) { // We could also select children of the search node here if we wanted to } -function isNodeSelected(node: NodeApi, tree: TreeApi) { +function isNodeHighlighted(node: NodeApi, tree: TreeApi) { // We treat no selection as all nodes being active. We may add some nuance later if (tree.selectedNodes.length == 0) { return true; } + for (const selectedNode of tree.selectedNodes) { if (isSelected(node.data, selectedNode.data)) { return true; } } + // Check if the parent node is highlighted + if (node.parent != null && isNodeHighlighted(node.parent, tree)) { + return true; + } + + return false; +} + +function isNodeActive(node: NodeApi, tree: TreeApi) { + for (const selectedNode of tree.selectedNodes) { + if (isSelected(node.data, selectedNode.data)) { + return true; + } + } return false; } function Node({ node, style, dragHandle, tree }: any) { - let selected: boolean = isNodeSelected(node, tree); + let highlighted: boolean = isNodeHighlighted(node, tree); + let active: boolean = isNodeActive(node, tree); return h( "div.node", { style, ref: dragHandle }, - h(EntityTag, { data: node.data, selected }) + h(EntityTag, { data: node.data, active, highlighted }) ); } diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts index 23c76b83..1531e0bd 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts @@ -21,21 +21,22 @@ function buildTags( selectedNodes: number[] ): AnnotateBlendTag[] { return highlights.map((highlight) => { - const isSelected = + const highlighted = selectedNodes.includes(highlight.id) || selectedNodes.length === 0; let color = highlight.backgroundColor; - if (!isSelected) { + if (!highlighted) { color = asChromaColor(color).alpha(0.2).css(); } return { color, markStyle: { - ...getTagStyle(highlight.backgroundColor, { selected: isSelected }), + ...getTagStyle(highlight.backgroundColor, { highlighted }), borderRadius: "0.2em", padding: "0.1em", fontWeight: 400, borderWidth: "1.5px", + cursor: "pointer", }, tagStyle: { display: "none", From 8231cf30c9165fab3b853f5bdcfcf77c5df5a223 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Mon, 14 Oct 2024 14:42:45 -0500 Subject: [PATCH 3/6] Updated selection criteria --- .../integrations/xdd/extractions/lib/index.ts | 13 ++++++-- .../integrations/xdd/extractions/lib/types.ts | 1 + .../feedback/@sourceTextID/lib/edit-state.ts | 1 - .../@sourceTextID/lib/text-visualizer.ts | 32 ++++++++++++------- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/pages/integrations/xdd/extractions/lib/index.ts b/pages/integrations/xdd/extractions/lib/index.ts index d51ad153..71a8f35b 100644 --- a/pages/integrations/xdd/extractions/lib/index.ts +++ b/pages/integrations/xdd/extractions/lib/index.ts @@ -5,8 +5,16 @@ import { Entity, EntityExt, Highlight, EntityType } from "./types"; import { CSSProperties } from "react"; import { asChromaColor } from "@macrostrat/color-utils"; -export function buildHighlights(entities: EntityExt[]): Highlight[] { +export function buildHighlights( + entities: EntityExt[], + parent: EntityExt | null +): Highlight[] { let highlights = []; + let parents = []; + if (parent != null) { + parents = [parent.id, ...(parent.parents ?? [])]; + } + for (const entity of entities) { highlights.push({ start: entity.indices[0], @@ -15,8 +23,9 @@ export function buildHighlights(entities: EntityExt[]): Highlight[] { backgroundColor: entity.type.color ?? "#ddd", tag: entity.type.name, id: entity.id, + parents, }); - highlights.push(...buildHighlights(entity.children ?? [])); + highlights.push(...buildHighlights(entity.children ?? [], entity)); } return highlights; } diff --git a/pages/integrations/xdd/extractions/lib/types.ts b/pages/integrations/xdd/extractions/lib/types.ts index 7ebde68c..379a5b9e 100644 --- a/pages/integrations/xdd/extractions/lib/types.ts +++ b/pages/integrations/xdd/extractions/lib/types.ts @@ -21,6 +21,7 @@ export type Highlight = { backgroundColor?: string; borderColor?: string; id: number; + parents?: number[]; }; export interface EntityExt extends Omit { diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts index b1c9ba7c..e44a2efb 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts @@ -52,7 +52,6 @@ export function useUpdatableTree( } function treeReducer(state: TreeState, action: TreeAction) { - console.log(action); switch (action.type) { case "move-node": // For each node in the tree, if the node is in the dragIds, remove it from the tree and collect it diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts index 1531e0bd..1144bf4f 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts @@ -5,7 +5,6 @@ import h from "./feedback.module.sass"; import { buildHighlights } from "#/integrations/xdd/extractions/lib"; import { Highlight } from "#/integrations/xdd/extractions/lib/types"; import { useCallback } from "react"; -import { asChromaColor } from "@macrostrat/color-utils"; import { getTagStyle } from "#/integrations/xdd/extractions/lib"; export interface FeedbackTextProps { @@ -21,20 +20,14 @@ function buildTags( selectedNodes: number[] ): AnnotateBlendTag[] { return highlights.map((highlight) => { - const highlighted = - selectedNodes.includes(highlight.id) || selectedNodes.length === 0; - let color = highlight.backgroundColor; - if (!highlighted) { - color = asChromaColor(color).alpha(0.2).css(); - } + const highlighted = isHighlighted(highlight, selectedNodes); + const active = isActive(highlight, selectedNodes); return { - color, markStyle: { - ...getTagStyle(highlight.backgroundColor, { highlighted }), + ...getTagStyle(highlight.backgroundColor, { highlighted, active }), borderRadius: "0.2em", padding: "0.1em", - fontWeight: 400, borderWidth: "1.5px", cursor: "pointer", }, @@ -46,14 +39,29 @@ function buildTags( }); } +function isActive(tag: Highlight, selectedNodes: number[]) { + return selectedNodes.includes(tag.id); +} + +function isHighlighted(tag: Highlight, selectedNodes: number[]) { + if (selectedNodes.length === 0) return true; + return ( + (selectedNodes.includes(tag.id) || + tag.parents?.some((d) => selectedNodes.includes(d))) ?? + false + ); +} + export function FeedbackText(props: FeedbackTextProps) { // Convert input to tags const { text, selectedNodes, nodes, dispatch } = props; let allTags: AnnotateBlendTag[] = buildTags( - buildHighlights(nodes), + buildHighlights(nodes, null), selectedNodes ); + console.log("All tags", allTags); + const onChange = useCallback( (tags) => { // New tags @@ -83,7 +91,7 @@ export function FeedbackText(props: FeedbackTextProps) { return h(TextAnnotateBlend, { style: { - fontSize: "1.2rem", + fontSize: "1.2em", }, className: "feedback-text", content: text, From 9752aa9302e4fc7ea1167db845f57ad3d8818320 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Mon, 14 Oct 2024 17:28:40 -0500 Subject: [PATCH 4/6] Updated text creation --- .../integrations/xdd/extractions/lib/index.ts | 1 - .../feedback/@sourceTextID/+Page.client.ts | 6 - .../feedback/@sourceTextID/lib/edit-state.ts | 124 +++++++++++++++++- .../xdd/feedback/@sourceTextID/lib/index.ts | 43 +++++- .../@sourceTextID/lib/text-visualizer.ts | 2 - 5 files changed, 157 insertions(+), 19 deletions(-) diff --git a/pages/integrations/xdd/extractions/lib/index.ts b/pages/integrations/xdd/extractions/lib/index.ts index 71a8f35b..7e79263e 100644 --- a/pages/integrations/xdd/extractions/lib/index.ts +++ b/pages/integrations/xdd/extractions/lib/index.ts @@ -31,7 +31,6 @@ export function buildHighlights( } export function enhanceData(extractionData, models, entityTypes) { - console.log(extractionData, models); return { ...extractionData, model: models.get(extractionData.model_id), diff --git a/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts index f61368af..419631e6 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts @@ -30,12 +30,6 @@ export function Page() { ); } -const useStore = create((set) => { - return { - entities: null, - }; -}); - function ExtractionIndex() { const { routeParams } = usePageContext(); const { sourceTextID } = routeParams; diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts index e44a2efb..2cebc40f 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts @@ -1,7 +1,9 @@ import { TreeData } from "./types"; -import { Dispatch, useReducer } from "react"; +import { Dispatch, useCallback, useReducer } from "react"; import update, { Spec } from "immutability-helper"; import { EntityType } from "#/integrations/xdd/extractions/lib/data-service"; +import { EntityExt } from "#/integrations/xdd/extractions/lib/types"; +import * as string_decoder from "node:string_decoder"; interface TreeState { initialTree: TreeData[]; @@ -18,6 +20,11 @@ type TextRange = { text: string; }; +type TreeAsyncAction = { + type: "save"; + tree: TreeData[]; +}; + type TreeAction = | { type: "move-node"; @@ -30,7 +37,7 @@ type TreeAction = | { type: "select-entity-type"; payload: EntityType } | { type: "reset" }; -export type TreeDispatch = Dispatch; +export type TreeDispatch = Dispatch; export function useUpdatableTree( initialTree: TreeData[], @@ -48,7 +55,32 @@ export function useUpdatableTree( lastInternalId: 0, }); - return [state, dispatch]; + const handler = useCallback( + (action: TreeAsyncAction | TreeAction) => { + treeActionHandler(action).then((action) => { + if (action == null) return; + dispatch(action); + }); + }, + [dispatch] + ); + + return [state, handler]; +} + +async function treeActionHandler( + action: TreeAsyncAction | TreeAction +): Promise { + switch (action.type) { + case "save": + // Save the tree to the server + const data = prepareDataForServer(action.tree); + console.log(JSON.stringify(data, null, 2)); + + return null; + default: + return action; + } } function treeReducer(state: TreeState, action: TreeAction) { @@ -218,3 +250,89 @@ function removeNodes( return [newTree, removedNodes]; } + +export interface EntityOutput { + id: number; + type: number | null; + indices: number[]; + name: string; + match: MatchInfo | null; + reasoning: string | null; +} + +// We will extend this in the future, probably, +// to handle ages and other things +type MatchInfo = { type: "lith" | "lith_att" | "strat_name"; id: number }; + +interface GraphData { + nodes: EntityOutput[]; + edges: { source: number; dest: number }[]; +} + +interface ServerResults extends GraphData { + sourceTextId: number; + supersedesRunId: number; +} + +function normalizeMatch(match: any): MatchInfo | null { + if (match == null) return null; + if (match.lith_id) return { type: "lith", id: match.lith_id }; + if (match.lith_att_id) { + return { type: "lith_att", id: match.lith_att_id }; + } + if (match.strat_name_id) { + return { type: "strat_name", id: match.strat_name_id }; + } + return null; +} + +function prepareGraphForServer(tree: TreeData[]): GraphData { + // Convert the tree to a graph + let nodes: EntityOutput[] = []; + let edges: { source: number; dest: number }[] = []; + const nodeMap = new Map(); + + for (let node of tree) { + // If we've already found an instance of this node, we don't need to record + // it again + if (nodeMap.has(node.id)) { + continue; + } + + const { indices, id, name } = node; + + const nodeData: EntityOutput = { + id, + type: node.type.id, + name, + indices, + reasoning: null, + match: normalizeMatch(node.match), + }; + + nodeMap.set(node.id, node); + + nodes.push(nodeData); + + if (node.children) { + for (let child of node.children) { + edges.push({ source: node.id, dest: child.id }); + } + + // Now process the children + const { nodes: childNodes, edges: childEdges } = prepareGraphForServer( + node.children + ); + nodes.push(...childNodes); + edges.push(...childEdges); + } + } + + return { nodes, edges }; +} + +function prepareDataForServer(state: TreeData[]): ServerResults { + /** This function should be used before sending the data to the server */ + const { nodes, edges } = prepareGraphForServer(state); + return { nodes, edges, sourceTextId: 0, supersedesRunId: 0 }; +} diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts index dc367f79..9b10a265 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts @@ -1,5 +1,4 @@ -import hyper from "@macrostrat/hyper"; -import styles from "./feedback.module.sass"; +import h from "./feedback.module.sass"; import { NodeApi, Tree, TreeApi } from "react-arborist"; import Node from "./node"; import { FeedbackText } from "./text-visualizer"; @@ -7,12 +6,10 @@ import { Entity, Result, TextData, TreeData, InternalEntity } from "./types"; import { ModelInfo } from "#/integrations/xdd/extractions/lib"; import { useUpdatableTree } from "./edit-state"; import { useEffect, useRef, useState } from "react"; -import { ValueWithUnit } from "@macrostrat/map-interface"; import { DataField } from "~/components/unit-details"; -import { Card } from "@blueprintjs/core"; -import { OmniboxSelector } from "#/integrations/xdd/feedback/@sourceTextID/lib/type-selector"; - -const h = hyper.styled(styles); +import { ButtonGroup, Card } from "@blueprintjs/core"; +import { OmniboxSelector } from "./type-selector"; +import { CancelButton, SaveButton } from "@macrostrat/ui-components"; export interface FeedbackComponentProps { // Add props here @@ -46,6 +43,38 @@ export function FeedbackComponent({ entities = [], text, model, entityTypes }) { h(ModelInfo, { data: model }), h("div.entity-panel", [ h(Card, { className: "control-panel" }, [ + h( + ButtonGroup, + { + vertical: true, + fill: true, + minimal: true, + alignText: "left", + }, + [ + h( + CancelButton, + { + icon: "trash", + disabled: state.initialTree == state.tree, + onClick() { + dispatch({ type: "reset" }); + }, + }, + "Reset" + ), + h( + SaveButton, + { + onClick() { + dispatch({ type: "save", tree }); + }, + disabled: state.initialTree == state.tree, + }, + "Save" + ), + ] + ), h(EntityTypeSelector, { entityTypes, selected: selectedEntityType, diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts index 1144bf4f..2ea38be3 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts @@ -60,8 +60,6 @@ export function FeedbackText(props: FeedbackTextProps) { selectedNodes ); - console.log("All tags", allTags); - const onChange = useCallback( (tags) => { // New tags From bb1e51e97e4c9d30957d511f0b9ab6db821d75d0 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Mon, 14 Oct 2024 17:54:20 -0500 Subject: [PATCH 5/6] Updated and simplified tagging UI --- .../feedback/@sourceTextID/+Page.client.ts | 68 ++++---- .../feedback/@sourceTextID/lib/edit-state.ts | 10 +- .../feedback/@sourceTextID/lib/fetch-data.ts | 123 -------------- .../xdd/feedback/@sourceTextID/lib/index.ts | 20 ++- .../@sourceTextID/lib/record-feedback.ts | 154 ------------------ .../@sourceTextID/lib/text-visualizer.ts | 20 ++- .../@sourceTextID/lib/type-selector/index.ts | 2 - 7 files changed, 68 insertions(+), 329 deletions(-) delete mode 100644 pages/integrations/xdd/feedback/@sourceTextID/lib/fetch-data.ts delete mode 100644 pages/integrations/xdd/feedback/@sourceTextID/lib/record-feedback.ts diff --git a/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts index 419631e6..18f301f5 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts @@ -2,23 +2,19 @@ import h from "@macrostrat/hyper"; import { ContentPage } from "~/layouts"; import { PageBreadcrumbs } from "~/components"; import { usePageContext } from "vike-react/usePageContext"; -import { enhanceData, ExtractionContext } from "../../extractions/lib"; +import { enhanceData } from "../../extractions/lib"; import { - usePostgresQuery, - useModelIndex, useEntityTypeIndex, + useModelIndex, + usePostgresQuery, } from "../../extractions/lib/data-service"; import { FeedbackComponent } from "./lib"; -import { JSONView } from "@macrostrat/ui-components"; -import { create } from "zustand"; -import { useEffect } from "react"; -import { NonIdealState, OverlaysProvider, Spinner } from "@blueprintjs/core"; +import { OverlaysProvider } from "@blueprintjs/core"; /** * Get a single text window for feedback purposes */ -// noinspection JSUnusedGlobalSymbols export function Page() { return h( OverlaysProvider, @@ -42,37 +38,37 @@ function ExtractionIndex() { predicate: sourceTextID, }); - useEffect(() => { - if (data == null) return; - useStore.setState({ entities: data[0]?.entities }); - }, [data]); - if (data == null || models == null || entityTypes == null) { return h("div", "Loading..."); } - const window = enhanceData(data[0], models, entityTypes); - const { entities = [], paragraph_text, model } = window; - - return h([ - //h("h1", paper.citation?.title ?? "Model extractions"), - h(FeedbackComponent, { - entities, - text: paragraph_text, - model, - entityTypes, - }), - ]); -} - -function FeedbackDevTool() { - const entities = useStore((state) => state.entities); - if (entities == null) - return h(NonIdealState, { icon: h(Spinner), title: "Loading..." }); - - return h(JSONView, { data: entities, showRoot: false, keyPath: 0 }); + return h( + "div.feedback-windows", + data.map((d) => { + console.log(data); + const window = enhanceData(d, models, entityTypes); + const { entities = [], paragraph_text, model } = window; + //h("h1", paper.citation?.title ?? "Model extractions"), + return h(FeedbackComponent, { + entities, + text: paragraph_text, + model, + entityTypes, + sourceTextID: window.source_text, + runID: window.model_run, + }); + }) + ); } -FeedbackDevTool.title = "Feedback"; - -export const devTools = [FeedbackDevTool]; +// function FeedbackDevTool() { +// const entities = useStore((state) => state.entities); +// if (entities == null) +// return h(NonIdealState, { icon: h(Spinner), title: "Loading..." }); +// +// return h(JSONView, { data: entities, showRoot: false, keyPath: 0 }); +// } +// +// FeedbackDevTool.title = "Feedback"; +// +// export const devTools = [FeedbackDevTool]; diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts index 2cebc40f..f1d04ff8 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts @@ -2,8 +2,6 @@ import { TreeData } from "./types"; import { Dispatch, useCallback, useReducer } from "react"; import update, { Spec } from "immutability-helper"; import { EntityType } from "#/integrations/xdd/extractions/lib/data-service"; -import { EntityExt } from "#/integrations/xdd/extractions/lib/types"; -import * as string_decoder from "node:string_decoder"; interface TreeState { initialTree: TreeData[]; @@ -254,7 +252,7 @@ function removeNodes( export interface EntityOutput { id: number; type: number | null; - indices: number[]; + txt_range: number[][]; name: string; match: MatchInfo | null; reasoning: string | null; @@ -271,7 +269,7 @@ interface GraphData { interface ServerResults extends GraphData { sourceTextId: number; - supersedesRunId: number; + supersedesRunIds: number[]; } function normalizeMatch(match: any): MatchInfo | null { @@ -305,7 +303,7 @@ function prepareGraphForServer(tree: TreeData[]): GraphData { id, type: node.type.id, name, - indices, + txt_range: [indices], reasoning: null, match: normalizeMatch(node.match), }; @@ -334,5 +332,5 @@ function prepareGraphForServer(tree: TreeData[]): GraphData { function prepareDataForServer(state: TreeData[]): ServerResults { /** This function should be used before sending the data to the server */ const { nodes, edges } = prepareGraphForServer(state); - return { nodes, edges, sourceTextId: 0, supersedesRunId: 0 }; + return { nodes, edges, sourceTextId: 0, supersedesRunIds: null }; } diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/fetch-data.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/fetch-data.ts deleted file mode 100644 index 599027d2..00000000 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/fetch-data.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Entity, Result, ServerRelationship, ServerResponse } from "./types"; - -function addToMap( - paragraph_txt: string, - entity_name: string, - entity_type: string, - terms_map: Map -) { - // We have already processed this term - if (terms_map.has(entity_name)) { - return; - } - - // Ensure this string actually exists - let start_idx = paragraph_txt - .toLowerCase() - .indexOf(entity_name.toLowerCase()); - if (start_idx == -1) { - return; - } - let end_idx = start_idx + entity_name.length; - - // Create the entity - let entity_to_add: Entity = { - term_type: entity_type, - txt_range: [[start_idx, end_idx]], - children: [], - }; - terms_map.set(entity_name, entity_to_add); -} - -function mergeChild( - src_map: Map, - dst_map: Map, - relationship: ServerRelationship -) { - // Get the child - let child_entity: Entity | undefined = dst_map.get(relationship.dst_name); - if (!child_entity) { - return; - } - - // Get the parent - let parent_entity: Entity | undefined = src_map.get(relationship.src_name); - if (!parent_entity) { - return; - } - - // Merge child with parent - dst_map.delete(relationship.dst_name); - parent_entity.children?.push(child_entity); - src_map.set(relationship.src_name, parent_entity); -} - -function convertToTree(responseJson: ServerResponse): Result { - let strats_map: Map = new Map(); - let lith_maps: Map = new Map(); - let att_maps: Map = new Map(); - let paragraph_txt = responseJson.text.paragraph_text; - - // Create the initial entity - for (var curr_rel of responseJson.relationships) { - if (curr_rel.relationship_type == "strat_to_lith") { - addToMap(paragraph_txt, curr_rel.src_name, "strat_name", strats_map); - addToMap(paragraph_txt, curr_rel.dst_name, "lith_base", lith_maps); - } else if (curr_rel.relationship_type == "lith_to_attribute") { - addToMap(paragraph_txt, curr_rel.src_name, "lith_base", lith_maps); - addToMap(paragraph_txt, curr_rel.dst_name, "att_base", att_maps); - } - } - - // Merge attribute to liths - for (var curr_rel of responseJson.relationships) { - if (curr_rel.relationship_type == "lith_to_attribute") { - mergeChild(lith_maps, att_maps, curr_rel); - } - } - - // Merge liths to strats - for (var curr_rel of responseJson.relationships) { - if (curr_rel.relationship_type == "strat_to_lith") { - mergeChild(strats_map, lith_maps, curr_rel); - } - } - - // Deal with any provided just entities - for (var curr_entity of responseJson.just_entities) { - if (curr_entity.entity_type == "strat_name") { - addToMap( - paragraph_txt, - curr_entity.entity_name, - curr_entity.entity_type, - strats_map - ); - } - } - - // Create the result - let result_to_return: Result = { - text: responseJson.text, - strats: [], - }; - strats_map.forEach((value, key, map) => { - result_to_return.strats?.push(value); - }); - return result_to_return; -} - -export async function getExampleData(): Promise { - try { - const response = await fetch("http://cosmos0003.chtc.wisc.edu:3001/"); - - if (response.ok) { - const responseJson = await response.json(); - let result: Result = convertToTree(responseJson); - return result; - } else { - throw new Error("Failed to get example from server"); - } - } catch (error) { - throw error; - } -} diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts index 9b10a265..58476235 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts @@ -1,8 +1,8 @@ import h from "./feedback.module.sass"; -import { NodeApi, Tree, TreeApi } from "react-arborist"; +import { Tree, TreeApi } from "react-arborist"; import Node from "./node"; import { FeedbackText } from "./text-visualizer"; -import { Entity, Result, TextData, TreeData, InternalEntity } from "./types"; +import { Entity, InternalEntity, TreeData } from "./types"; import { ModelInfo } from "#/integrations/xdd/extractions/lib"; import { useUpdatableTree } from "./edit-state"; import { useEffect, useRef, useState } from "react"; @@ -23,7 +23,14 @@ function setsAreTheSame(a: Set, b: Set) { return true; } -export function FeedbackComponent({ entities = [], text, model, entityTypes }) { +export function FeedbackComponent({ + entities = [], + text, + model, + entityTypes, + sourceTextID, + runID, +}) { // Get the input arguments const [state, dispatch] = useUpdatableTree( @@ -67,7 +74,12 @@ export function FeedbackComponent({ entities = [], text, model, entityTypes }) { SaveButton, { onClick() { - dispatch({ type: "save", tree }); + dispatch({ + type: "save", + tree, + sourceTextID: sourceID, + runID: runID, + }); }, disabled: state.initialTree == state.tree, }, diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/record-feedback.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/record-feedback.ts deleted file mode 100644 index 997f5628..00000000 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/record-feedback.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { - RunEntity, - RunRecord, - RunRelationship, - RunSource, - RunText, - TextData, - TreeData, -} from "./types"; - -type LEVEL_TO_NAME_TYPE = { - [key: number]: string; - 0: string; - 1: string; - 2: string; -}; - -const LEVEL_TO_NODE_NAME: LEVEL_TO_NAME_TYPE = { - 0: "strat_name", - 1: "lith", - 2: "lith_att", -}; - -const LEVEL_TO_RELATIONSHIP_NAME: LEVEL_TO_NAME_TYPE = { - 0: "strat_to_lith", - 1: "lith_to_attribute", - 2: "", -}; - -function get_node_type(node: TreeData, map: LEVEL_TO_NAME_TYPE): string { - let id_parts: string[] = node.id.split("_"); - let level = parseInt(id_parts[0]); - return map[level]; -} - -function runDFS( - node: TreeData, - relationships: RunRelationship[], - processed_nodes: Set -) { - // We reached a leaf node - if (node.children.length == 0) { - return; - } - - // Not a valid relationship type - let relationship_type = get_node_type(node, LEVEL_TO_RELATIONSHIP_NAME); - if (relationship_type.length == 0) { - return; - } - - let src_name: string = node.name; - processed_nodes.add(src_name); - for (var child_node of node.children) { - // Record the relationship - relationships.push({ - src: src_name, - dst: child_node.name, - relationship_type: relationship_type, - }); - processed_nodes.add(child_node.name); - - runDFS(child_node, relationships, processed_nodes); - } -} - -function getDateString(): string { - let datetime = new Date(); - let year = datetime.getFullYear(); - let month = String(datetime.getMonth() + 1).padStart(2, "0"); - let day = String(datetime.getDate()).padStart(2, "0"); - let hours = String(datetime.getHours()).padStart(2, "0"); - let minutes = String(datetime.getMinutes()).padStart(2, "0"); - let seconds = String(datetime.getSeconds()).padStart(2, "0"); - let milliseconds = String(datetime.getMilliseconds()).padStart(3, "0"); - - return `${year}-${month}-${day}_${hours}:${minutes}:${seconds}.${milliseconds}`; -} - -export async function recordFeedback( - text: TextData, - tree: TreeData[] -): Promise { - let root_node: TreeData = tree[0]; - let processed_nodes: Set = new Set(); - let relationships: RunRelationship[] = []; - // Extract relationships by dfs the tree - for (var node of root_node.children) { - runDFS(node, relationships, processed_nodes); - } - - let just_entities: RunEntity[] = []; - for (var node of root_node.children) { - // Only record the strats - let node_type = get_node_type(node, LEVEL_TO_NODE_NAME); - if (node_type != LEVEL_TO_NODE_NAME[0]) { - just_entities.push({ - entity: node.name, - entity_type: node_type, - }); - } - } - - // Create the result - let run_text: RunText = { - preprocessor_id: text.preprocessor_id, - paper_id: text.paper_id, - hashed_text: text.hashed_text, - weaviate_id: text.weaviate_id, - paragraph_text: text.paragraph_text, - }; - - let run_source: RunSource = { - text: run_text, - relationships: relationships, - just_entities: just_entities, - }; - - let date_string: string = getDateString(); - let run_id = "storybook_cosmos0003_user_feedback_" + date_string; - let user_name = "storybook_cosmos003_feedback_user_" + date_string; - let run_to_record: RunRecord = { - run_id: run_id, - extraction_pipeline_id: text.extraction_pipeline_id, - user_name: user_name, - model_id: text.model_id, - results: [run_source], - }; - - // Make the fetch request - let requestOptions = { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(run_to_record), - }; - console.log("Sending request of", run_to_record); - - try { - // Make sure the response is okay - const response = await fetch( - "http://cosmos0003.chtc.wisc.edu:9543/record_run", - requestOptions - ); - if (!response.ok) { - throw new Error( - "Server returned response code of " + response.status.toString() - ); - } - - return true; - } catch (error) { - throw error; - } -} diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts index 2ea38be3..905f0eaf 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/text-visualizer.ts @@ -19,11 +19,19 @@ function buildTags( highlights: Highlight[], selectedNodes: number[] ): AnnotateBlendTag[] { - return highlights.map((highlight) => { + let tags: AnnotateBlendTag[] = []; + + // If entity ID has already been seen, don't add it again + const entities = new Set(); + + for (const highlight of highlights) { + // Don't add multiply-linked entities multiple times + if (entities.has(highlight.id)) continue; + const highlighted = isHighlighted(highlight, selectedNodes); const active = isActive(highlight, selectedNodes); - return { + tags.push({ markStyle: { ...getTagStyle(highlight.backgroundColor, { highlighted, active }), borderRadius: "0.2em", @@ -35,8 +43,12 @@ function buildTags( display: "none", }, ...highlight, - }; - }); + }); + + entities.add(highlight.id); + } + + return tags; } function isActive(tag: Highlight, selectedNodes: number[]) { diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/type-selector/index.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/type-selector/index.ts index 16971b65..7ecc570b 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/type-selector/index.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/type-selector/index.ts @@ -5,9 +5,7 @@ import h from "./main.module.sass"; import classNames from "classnames"; import { Omnibar, OmnibarProps } from "@blueprintjs/select"; -import chroma from "chroma-js"; import "@blueprintjs/select/lib/css/blueprint-select.css"; -import { useDarkMode, useInDarkMode } from "@macrostrat/ui-components"; interface TagItemProps { selected: boolean; From 038dd9d6fe5b41818189b7ae76417c50f5bc3337 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Mon, 14 Oct 2024 17:59:48 -0500 Subject: [PATCH 6/6] Updated state management --- .../feedback/@sourceTextID/lib/edit-state.ts | 23 +++++++++++++++---- .../xdd/feedback/@sourceTextID/lib/index.ts | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts index f1d04ff8..a37e8cbc 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts @@ -21,6 +21,8 @@ type TextRange = { type TreeAsyncAction = { type: "save"; tree: TreeData[]; + sourceTextID: number; + supersedesRunIDs: number[]; }; type TreeAction = @@ -72,7 +74,11 @@ async function treeActionHandler( switch (action.type) { case "save": // Save the tree to the server - const data = prepareDataForServer(action.tree); + const data = prepareDataForServer( + action.tree, + action.sourceTextID, + action.supersedesRunIDs + ); console.log(JSON.stringify(data, null, 2)); return null; @@ -329,8 +335,17 @@ function prepareGraphForServer(tree: TreeData[]): GraphData { return { nodes, edges }; } -function prepareDataForServer(state: TreeData[]): ServerResults { +function prepareDataForServer( + tree: TreeData[], + sourceTextID, + supersedesRunIDs +): ServerResults { /** This function should be used before sending the data to the server */ - const { nodes, edges } = prepareGraphForServer(state); - return { nodes, edges, sourceTextId: 0, supersedesRunIds: null }; + const { nodes, edges } = prepareGraphForServer(tree); + return { + nodes, + edges, + sourceTextId: sourceTextID, + supersedesRunIds: supersedesRunIDs ?? [], + }; } diff --git a/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts b/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts index 58476235..14ada7e1 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts @@ -77,8 +77,8 @@ export function FeedbackComponent({ dispatch({ type: "save", tree, - sourceTextID: sourceID, - runID: runID, + sourceTextID: sourceTextID, + supersedesRunIDs: [runID], }); }, disabled: state.initialTree == state.tree,