diff --git a/pages/integrations/xdd/extractions/lib/index.ts b/pages/integrations/xdd/extractions/lib/index.ts index 80652661..7e79263e 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,14 +23,14 @@ 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; } export function enhanceData(extractionData, models, entityTypes) { - console.log(extractionData, models); return { ...extractionData, model: models.get(extractionData.model_id), @@ -34,18 +42,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 +69,7 @@ export function getTagStyle( borderStyle: "solid", borderColor, borderWidth: "1px", + fontWeight: active ? "bold" : "normal", }; } @@ -101,7 +115,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,12 +125,12 @@ 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("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/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/+Page.client.ts b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts index 1bb8f026..18f301f5 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts @@ -2,28 +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 { - Card, - 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, @@ -35,12 +26,6 @@ export function Page() { ); } -const useStore = create((set) => { - return { - entities: null, - }; -}); - function ExtractionIndex() { const { routeParams } = usePageContext(); const { sourceTextID } = routeParams; @@ -53,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 500d600b..a37e8cbc 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/edit-state.ts @@ -1,5 +1,5 @@ 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"; @@ -18,6 +18,13 @@ type TextRange = { text: string; }; +type TreeAsyncAction = { + type: "save"; + tree: TreeData[]; + sourceTextID: number; + supersedesRunIDs: number[]; +}; + type TreeAction = | { type: "move-node"; @@ -27,9 +34,10 @@ 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; +export type TreeDispatch = Dispatch; export function useUpdatableTree( initialTree: TreeData[], @@ -47,11 +55,39 @@ 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, + action.sourceTextID, + action.supersedesRunIDs + ); + console.log(JSON.stringify(data, null, 2)); + + return null; + default: + return action; + } } 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 @@ -109,7 +145,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, @@ -129,11 +164,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); } @@ -143,6 +176,12 @@ function treeReducer(state: TreeState, action: TreeAction) { selectedEntityType: action.payload, }; } + case "reset": + return { + ...state, + tree: state.initialTree, + selectedNodes: [], + }; } } @@ -215,3 +254,98 @@ function removeNodes( return [newTree, removedNodes]; } + +export interface EntityOutput { + id: number; + type: number | null; + txt_range: 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; + supersedesRunIds: 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, + txt_range: [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( + tree: TreeData[], + sourceTextID, + supersedesRunIDs +): ServerResults { + /** This function should be used before sending the data to the server */ + const { nodes, edges } = prepareGraphForServer(tree); + return { + nodes, + edges, + sourceTextId: sourceTextID, + supersedesRunIds: supersedesRunIDs ?? [], + }; +} 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 0da6a82a..14ada7e1 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/lib/index.ts @@ -1,18 +1,15 @@ -import hyper from "@macrostrat/hyper"; -import styles from "./feedback.module.sass"; -import { NodeApi, Tree, TreeApi } from "react-arborist"; +import h from "./feedback.module.sass"; +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"; -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 @@ -26,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( @@ -46,6 +50,43 @@ 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, + sourceTextID: sourceTextID, + supersedesRunIDs: [runID], + }); + }, + disabled: state.initialTree == state.tree, + }, + "Save" + ), + ] + ), h(EntityTypeSelector, { entityTypes, selected: selectedEntityType, @@ -74,6 +115,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 +130,7 @@ function EntityTypeSelector({ entityTypes, selected, onChange }) { h(OmniboxSelector, { isOpen, items: Array.from(entityTypes.values()), - selectedItem: selected, + selectedItem: _selected, onSelectItem(item) { setOpen(false); onChange(item); 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/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 23c76b83..905f0eaf 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 { @@ -20,36 +19,56 @@ function buildTags( highlights: Highlight[], selectedNodes: number[] ): AnnotateBlendTag[] { - return highlights.map((highlight) => { - const isSelected = - selectedNodes.includes(highlight.id) || selectedNodes.length === 0; - let color = highlight.backgroundColor; - if (!isSelected) { - color = asChromaColor(color).alpha(0.2).css(); - } + let tags: AnnotateBlendTag[] = []; - return { - color, + // 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); + + tags.push({ markStyle: { - ...getTagStyle(highlight.backgroundColor, { selected: isSelected }), + ...getTagStyle(highlight.backgroundColor, { highlighted, active }), borderRadius: "0.2em", padding: "0.1em", - fontWeight: 400, borderWidth: "1.5px", + cursor: "pointer", }, tagStyle: { display: "none", }, ...highlight, - }; - }); + }); + + entities.add(highlight.id); + } + + return tags; +} + +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 ); @@ -82,7 +101,7 @@ export function FeedbackText(props: FeedbackTextProps) { return h(TextAnnotateBlend, { style: { - fontSize: "1.2rem", + fontSize: "1.2em", }, className: "feedback-text", content: text, 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;