diff --git a/pages/dev/legend-affinity/+Page.client.ts b/pages/dev/legend-affinity/+Page.client.ts index 16dd4b8b..d69f1c9a 100644 --- a/pages/dev/legend-affinity/+Page.client.ts +++ b/pages/dev/legend-affinity/+Page.client.ts @@ -1,10 +1,6 @@ import h from "./main.module.sass"; import { mapboxAccessToken, tileserverDomain } from "@macrostrat-web/settings"; -import { - buildQueryString, - useDarkMode, - useStoredState, -} from "@macrostrat/ui-components"; +import { useDarkMode } from "@macrostrat/ui-components"; import { Select, SelectProps } from "@blueprintjs/select"; import mapboxgl from "mapbox-gl"; import { useCallback, useState, useEffect, useMemo } from "react"; @@ -158,14 +154,6 @@ export function Page() { ); } -function isValidMapPosition(data: any): boolean { - if (data == null) return false; - if (typeof data !== "object") return false; - if (data?.camera?.lng == null) return false; - if (data?.camera?.lat == null) return false; - return true; -} - interface StyleParams { inDarkMode: boolean; term: string | null; @@ -360,6 +348,7 @@ function useMapPosition(startPos) { setMapPosition_(position); let params = getQueryString(window.location.search) ?? {}; applyMapPositionToHash(params, position); + console.log(params); setQueryString(params); }, []); diff --git a/pages/integrations/xdd/extractions/+Page.ts b/pages/integrations/xdd/extractions/+Page.ts index 1cafd56c..7b593b35 100644 --- a/pages/integrations/xdd/extractions/+Page.ts +++ b/pages/integrations/xdd/extractions/+Page.ts @@ -6,7 +6,11 @@ import { ContentPage } from "~/layouts"; import { PageHeaderV2 } from "~/components"; import { postgrestPrefix } from "@macrostrat-web/settings"; import { useEffect, useState } from "react"; -import { InfiniteScroll, LoadingPlaceholder } from "@macrostrat/ui-components"; +import { + AuthorList, + InfiniteScroll, + LoadingPlaceholder, +} from "@macrostrat/ui-components"; import { create } from "zustand"; const postgrest = new PostgrestClient(postgrestPrefix); @@ -33,8 +37,8 @@ const useStore = create((set, get) => ({ let req = postgrest .from("kg_publication_entities") - .select("citation,paper_id") - .order("paper_id", { ascending: true }); + .select("*") + .order("n_matches", { ascending: false }); if (lastID != null) { req = req.gt("paper_id", lastID); @@ -73,11 +77,21 @@ function ExtractionIndex() { ]); } +function NameMatch({ type, count, pluralSuffix = "s" }) { + let pluralType = type; + if (count > 1) { + pluralType += pluralSuffix; + } + + return `${count} ${pluralType}`; +} + function PaperList({ data }) { const ctx = usePageContext(); const pageLink = ctx.urlPathname; return h("div.paper-list", [ data.map((d) => { + console.log(d); return h("div", [ h(xDDCitation, { citation: d.citation, @@ -85,7 +99,11 @@ function PaperList({ data }) { }), h.if(d.n_matches != null)( "p", - `${d.n_matches} stratigraphic name matches` + h(NameMatch, { + type: "stratigraphic name match", + count: d.n_matches, + pluralSuffix: "es", + }) ), ]); }), @@ -114,6 +132,39 @@ function pruneEmptyCitationElements(citation): any { function xDDCitation({ citation, href }) { const newCitation = pruneEmptyCitationElements(citation); - const { title } = newCitation; - return h("div", [h("h2.title", h("a", { href }, title))]); + const { title, author, journal, identifier } = newCitation; + const names = author?.map((d) => d.name); + return h("div", [ + h("h2.title", h("a", { href }, title)), + h("h3.journal", null, journal), + h(AuthorList, { names }), + h(IdentLink, { identifier: getBestIdentifier(identifier) }), + ]); +} + +function IdentLink({ identifier }) { + if (identifier == null) return null; + const { type, id } = identifier; + + let ident = h("code.identifier", id); + if (type == "doi") { + ident = h("a", { href: "https://dx.doi.org/doi/" + id }, ident); + } + + return h("p", [h("span.label", type), " ", ident]); +} + +type Identifier = { + id: string; + type: string; +}; + +function getBestIdentifier(identifier: Identifier[] | null): Identifier | null { + if (identifier == null || identifier.length == 0) return null; + for (const ident of identifier) { + if (ident.type == "doi") { + return ident; + } + } + return identifier[0]; } diff --git a/pages/integrations/xdd/extractions/@paperId/+Page.ts b/pages/integrations/xdd/extractions/@paperId/+Page.ts index 41c34721..f8e8c3bc 100644 --- a/pages/integrations/xdd/extractions/@paperId/+Page.ts +++ b/pages/integrations/xdd/extractions/@paperId/+Page.ts @@ -1,55 +1,154 @@ -import h from "@macrostrat/hyper"; +import h from "./main.module.sass"; import { PostgrestClient } from "@supabase/postgrest-js"; import { ContentPage } from "~/layouts"; import { PageBreadcrumbs } from "~/components"; import { postgrestPrefix } from "@macrostrat-web/settings"; -import { useEffect, useState } from "react"; +import { useEffect, memo, useState } from "react"; import { usePageContext } from "vike-react/usePageContext"; -import { JSONView } from "@macrostrat/ui-components"; +import { Tag } from "@blueprintjs/core"; +import classNames from "classnames"; +import { asChromaColor } from "@macrostrat/color-utils"; const postgrest = new PostgrestClient(postgrestPrefix); -function usePostgresQuery(query, { paperId }) { +interface FilterDef { + subject: string; + op?: string; + predicate: any; +} + +function usePostgresQuery(query: string, filter: FilterDef | null = null) { const [data, setData] = useState(null); + useEffect(() => { - postgrest - .from(query) - .select() - .filter("paper_id", "eq", paperId) - .then((res) => { - setData(res.data); - }); + let q = postgrest.from(query).select(); + + if (filter != null) { + const { subject, op = "eq", predicate } = filter; + + q = q.filter(subject, op, predicate); + } + + q.then((res) => { + setData(res.data); + }); }, [query]); return data; } +function useIndex(model, idField = "id") { + const models = usePostgresQuery(model); + if (models == null) return null; + return new Map(models.map((d) => [d[idField], d])); +} + +function useModelIndex() { + return useIndex("kg_model"); +} + +function useEntityTypeIndex() { + const ix = useIndex("kg_entity_type"); + return ix; +} + export function Page() { return h(ContentPage, [h(PageBreadcrumbs), h(PageMain)]); } function PageMain() { - return h("div", [ - h("h1", "xDD stratigraphic name extractions"), - h(ExtractionIndex), - ]); + return h("div", [h(ExtractionIndex)]); } function ExtractionIndex() { const { routeParams } = usePageContext(); const { paperId } = routeParams; - const data = usePostgresQuery("kg_context_entities", { paperId }); - if (data == null) { + const models = useModelIndex(); + const entityTypes = useEntityTypeIndex(); + + const paper = usePostgresQuery("kg_publication_entities", { + subject: "paper_id", + predicate: paperId, + })?.[0]; + + const data = usePostgresQuery("kg_context_entities", { + subject: "paper_id", + predicate: paperId, + }); + + if (data == null || models == null || paper == null || entityTypes == null) { return h("div", "Loading..."); } - return h(data.map((d) => h(ExtractionContext, { data: d }))); + return h([ + h("h1", paper.citation?.title ?? "Model extractions"), + data.map((d) => { + return h(ExtractionContext, { + data: enhanceData(d, models, entityTypes), + entityTypes, + }); + }), + ]); +} + +function buildHighlights(entities, entityTypes): Highlight[] { + let highlights = []; + for (const entity of entities) { + console.log(entity); + highlights.push({ + start: entity.indices[0], + end: entity.indices[1], + backgroundColor: entity.type.color ?? "#ddd", + }); + highlights.push(...buildHighlights(entity.children ?? [], entityTypes)); + } + return highlights; +} + +function enhanceData(extractionData, models, entityTypes) { + console.log(entityTypes); + return { + ...extractionData, + model: models.get(extractionData.model_id), + entities: extractionData.entities?.map((d) => + enhanceEntity(d, entityTypes) + ), + }; } -function ExtractionContext({ data }) { +function enhanceEntity(entity, entityTypes) { + console.log(entity); + return { + ...entity, + type: addColor(entityTypes.get(entity.type), entity.match != null), + children: entity.children?.map((d) => enhanceEntity(d, entityTypes)), + }; +} + +function addColor(entityType, match = false) { + let color = "#ddd"; + const name = entityType.name; + if (name == "strat_name") color = "#be75c6"; + + if (name == "lith") color = "#74ea41"; + + if (name == "strat_noun") color = "#be75c6"; + + if (name == "lith_att") color = "#e8e534"; + + color = asChromaColor(color).brighten(match ? 1 : 2); + + return { ...entityType, color: color.css() }; +} + +function ExtractionContext({ data, entityTypes }) { + const { name } = data.model; + const highlights = buildHighlights(data.entities, entityTypes); + return h("div", [ - h("p", data.paragraph_text), + h("p", h(HighlightedText, { text: data.paragraph_text, highlights })), + h("p.model-name", ["Model: ", h("code.bp5-code", name)]), h( "ul.entities", data.entities.map((d) => h(ExtractionInfo, { data: d })) @@ -62,7 +161,7 @@ type Match = any; interface Entity { id: number; name: string; - type: "strat_name" | "lith" | "lith_att"; + type?: number; indices: [number, number]; children: Entity[]; match?: Match; @@ -72,12 +171,22 @@ function ExtractionInfo({ data }: { data: Entity }) { const children = data.children ?? []; const match = data.match ?? null; + const className = classNames({ + matched: match != null, + type: data.type.name, + }); - console.log(data); - - return h("li.entity", { className: data.type }, [ - h("span.name", data.name), - h(Match, { data: match }), + return h("li.entity", { className }, [ + h( + Tag, + { style: { backgroundColor: data.type.color ?? "#ddd", color: "#222" } }, + [ + h("span.name", data.name), + ": ", + h("code.type", null, data.type.name), + h(Match, { data: match }), + ] + ), h.if(children.length > 0)([ h( "ul.children", @@ -90,7 +199,7 @@ function ExtractionInfo({ data }: { data: Entity }) { function Match({ data }) { if (data == null) return null; const href = buildHref(data); - return h([" ", h("a.match", { href }, data.name)]); + return h([" ", h("a.match", { href }, `#${matchID(data)}`)]); } function buildHref(match) { @@ -111,3 +220,46 @@ function buildHref(match) { return null; } + +function matchID(match) { + if (match == null) return null; + + for (const id of ["strat_name_id", "lith_id", "lith_att_id"]) { + if (match[id]) { + return match[id]; + } + } + return null; +} + +type Highlight = { + start: number; + end: number; + backgroundColor?: string; + borderColor?: string; +}; + +function HighlightedText(props: { text: string; highlights: Highlight[] }) { + const { text, highlights = [] } = props; + const parts = []; + let start = 0; + + const sortedHighlights = highlights.sort((a, b) => a.start - b.start); + const deconflictedHighlights = sortedHighlights.map((highlight, i) => { + if (i === 0) return highlight; + const prev = sortedHighlights[i - 1]; + if (highlight.start < prev.end) { + highlight.start = prev.end; + } + return highlight; + }); + + for (const highlight of deconflictedHighlights) { + const { start: s, end, ...rest } = highlight; + parts.push(text.slice(start, s)); + parts.push(h("span.highlight", { style: rest }, text.slice(s, end))); + start = end; + } + parts.push(text.slice(start)); + return h("span", parts); +} diff --git a/pages/integrations/xdd/extractions/@paperId/main.module.sass b/pages/integrations/xdd/extractions/@paperId/main.module.sass new file mode 100644 index 00000000..d378eb6a --- /dev/null +++ b/pages/integrations/xdd/extractions/@paperId/main.module.sass @@ -0,0 +1,8 @@ +.entities + list-style: none + padding-left: 0 + .entities ul + list-style: none + +.entity + margin: 0.2em 0 0.5em diff --git a/pages/dev/feedback/+Page.ts b/pages/integrations/xdd/feedback/+Page.client.ts similarity index 100% rename from pages/dev/feedback/+Page.ts rename to pages/integrations/xdd/feedback/+Page.client.ts