diff --git a/src/components/DownloadDialog.tsx b/src/components/DownloadDialog.tsx index 2420363..587d05d 100644 --- a/src/components/DownloadDialog.tsx +++ b/src/components/DownloadDialog.tsx @@ -1,48 +1,24 @@ -import React from 'react'; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Radio, - RadioGroup, - FormControlLabel, - FormControl, - TextField, -} from '@mui/material'; -import Papa from 'papaparse'; - -const jsonToCsvString = (json: any[] | Papa.UnparseObject) => - Promise.resolve(Papa.unparse(json)); +import React from "react"; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Radio, RadioGroup, FormControlLabel, FormControl, TextField } from "@mui/material"; +import Papa from "papaparse"; + +const jsonToCsvString = (json: any[] | Papa.UnparseObject) => Promise.resolve(Papa.unparse(json)); const constructPmidOrPmcLink = (id: string) => { - if (id.startsWith('PMID')) { - return `https://pubmed.ncbi.nlm.nih.gov/${id.split(':')[1]}`; + if (id.startsWith("PMID")) { + return `https://pubmed.ncbi.nlm.nih.gov/${id.split(":")[1]}`; } - if (id.startsWith('PMC')) { - return `https://pmc.ncbi.nlm.nih.gov/articles/${id.split(':')[1]}`; + if (id.startsWith("PMC")) { + return `https://pmc.ncbi.nlm.nih.gov/articles/${id.split(":")[1]}`; } - return ''; + return ""; }; -const getConcatPublicationsForResult = ( - result: { analyses: { edge_bindings: { [s: string]: unknown } | ArrayLike }[] }, - message: { knowledge_graph: { edges: { [x: string]: any } } } -) => { +const getConcatPublicationsForResult = (result: { analyses: { edge_bindings: { [s: string]: unknown } | ArrayLike }[] }, message: { knowledge_graph: { edges: { [x: string]: any } } }) => { const edgeIds = Object.values(result.analyses[0].edge_bindings) .flat() .map((e) => (e as { id: string }).id); - const publications = edgeIds - .flatMap((edgeId) => - message.knowledge_graph.edges[edgeId].attributes - .filter( - (attr: { attribute_type_id: string }) => attr.attribute_type_id === 'biolink:publications' - ) - .flatMap((attr: { value: any }) => attr.value) - ) - .map(constructPmidOrPmcLink); + const publications = edgeIds.flatMap((edgeId) => message.knowledge_graph.edges[edgeId].attributes.filter((attr: { attribute_type_id: string }) => attr.attribute_type_id === "biolink:publications").flatMap((attr: { value: any }) => attr.value)).map(constructPmidOrPmcLink); return publications; }; @@ -54,84 +30,65 @@ const constructCsvObj = (message: { nodes: { [x: string]: any }; }; }) => { - const nodeLabelHeaders = Object.keys(message.results[0].node_bindings).flatMap((node_label) => [ - `${node_label} (Name)`, - `${node_label} (CURIE)`, - ]); + const nodeLabelHeaders = Object.keys(message.results[0].node_bindings).flatMap((node_label) => [`${node_label} (Name)`, `${node_label} (CURIE)`]); let csvHeaderEdgeLabelsMerged = new Set(); - message.results.forEach( - (result: { - node_bindings: ArrayLike | { [s: string]: unknown }; - analyses: { edge_bindings: { [s: string]: unknown } | ArrayLike }[]; - }) => { - const curieToNodeLabel: Record = {}; - - Object.entries(result.node_bindings).forEach(([nodeLabel, nb]) => { - const curie = (nb as Array<{ id: string }>)[0].id; - curieToNodeLabel[curie] = nodeLabel; + message.results.forEach((result: { node_bindings: ArrayLike | { [s: string]: unknown }; analyses: { edge_bindings: { [s: string]: unknown } | ArrayLike }[] }) => { + const curieToNodeLabel: Record = {}; + + Object.entries(result.node_bindings).forEach(([nodeLabel, nb]) => { + const curie = (nb as Array<{ id: string }>)[0].id; + curieToNodeLabel[curie] = nodeLabel; + }); + + Object.values(result.analyses[0].edge_bindings) + .flat() + .forEach((eb) => { + const { subject, object } = message.knowledge_graph.edges[(eb as { id: string }).id]; + const subjectLabel = curieToNodeLabel[subject]; + const objectLabel = curieToNodeLabel[object]; + const csvHeaderEdgeLabel = `${subjectLabel} -> ${objectLabel}`; + if (subjectLabel && objectLabel) { + // TODO: These were occasionally returning undefined, figure out why + csvHeaderEdgeLabelsMerged.add(csvHeaderEdgeLabel); + } }); - - Object.values(result.analyses[0].edge_bindings) - .flat() - .forEach((eb) => { - const { subject, object } = message.knowledge_graph.edges[(eb as { id: string }).id]; - const subjectLabel = curieToNodeLabel[subject]; - const objectLabel = curieToNodeLabel[object]; - const csvHeaderEdgeLabel = `${subjectLabel} -> ${objectLabel}`; - if (subjectLabel && objectLabel) { - // TODO: These were occasionally returning undefined, figure out why - csvHeaderEdgeLabelsMerged.add(csvHeaderEdgeLabel); - } - }); - } - ); + }); const csvHeaderEdgeLabelsMergedArray = Array.from(csvHeaderEdgeLabelsMerged); - const header = [...nodeLabelHeaders, ...csvHeaderEdgeLabelsMergedArray, 'Score', 'Publications']; - - const body = message.results.map( - (result: { - node_bindings: ArrayLike | { [s: string]: unknown }; - analyses: { edge_bindings: { [s: string]: unknown } | ArrayLike }[]; - score: any; - }) => { - const row = new Array(header.length).fill(''); - const curieToNodeLabel: Record = {}; - Object.entries(result.node_bindings).forEach(([nodeLabel, nb], i) => { - const curie = (nb as Array<{ id: string }>)[0].id; - curieToNodeLabel[curie] = nodeLabel; - const node = message.knowledge_graph.nodes[curie]; - row[i * 2] = node.name || node.categories[0]; - row[i * 2 + 1] = curie; + const header = [...nodeLabelHeaders, ...csvHeaderEdgeLabelsMergedArray, "Score", "Publications"]; + + const body = message.results.map((result: { node_bindings: ArrayLike | { [s: string]: unknown }; analyses: { edge_bindings: { [s: string]: unknown } | ArrayLike }[]; score: any }) => { + const row = new Array(header.length).fill(""); + const curieToNodeLabel: Record = {}; + Object.entries(result.node_bindings).forEach(([nodeLabel, nb], i) => { + const curie = (nb as Array<{ id: string }>)[0].id; + curieToNodeLabel[curie] = nodeLabel; + const node = message.knowledge_graph.nodes[curie]; + row[i * 2] = node.name || node.categories[0]; + row[i * 2 + 1] = curie; + }); + + Object.values(result.analyses[0].edge_bindings) + .flat() + .forEach((eb) => { + const { subject, object, predicate, sources } = message.knowledge_graph.edges[(eb as { id: string }).id]; + const subjectLabel = curieToNodeLabel[subject]; + const objectLabel = curieToNodeLabel[object]; + if (subjectLabel && objectLabel) { + const csvHeaderEdgeLabel = `${curieToNodeLabel[subject]} -> ${curieToNodeLabel[object]}`; + const edgeHeaderIndex = header.findIndex((h) => h === csvHeaderEdgeLabel); + const primarySourceObj = sources.find((s: { resource_role: string }) => s.resource_role === "primary_knowledge_source"); + const primarySource = (primarySourceObj && primarySourceObj.resource_id) || undefined; + row[edgeHeaderIndex] += `${row[edgeHeaderIndex].length > 0 ? "\n" : ""}${predicate}${primarySource ? ` (${primarySource})` : ""}`; + } }); - Object.values(result.analyses[0].edge_bindings) - .flat() - .forEach((eb) => { - const { subject, object, predicate, sources } = - message.knowledge_graph.edges[(eb as { id: string }).id]; - const subjectLabel = curieToNodeLabel[subject]; - const objectLabel = curieToNodeLabel[object]; - if (subjectLabel && objectLabel) { - const csvHeaderEdgeLabel = `${curieToNodeLabel[subject]} -> ${curieToNodeLabel[object]}`; - const edgeHeaderIndex = header.findIndex((h) => h === csvHeaderEdgeLabel); - const primarySourceObj = sources.find( - (s: { resource_role: string }) => s.resource_role === 'primary_knowledge_source' - ); - const primarySource = (primarySourceObj && primarySourceObj.resource_id) || undefined; - row[edgeHeaderIndex] += `${row[edgeHeaderIndex].length > 0 ? '\n' : ''}${predicate}${ - primarySource ? ` (${primarySource})` : '' - }`; - } - }); - - row[row.length - 2] = result.score; - row[row.length - 1] = getConcatPublicationsForResult(result, message).join('\n'); - - return row; - } - ); + row[row.length - 2] = result.score; + row[row.length - 1] = getConcatPublicationsForResult(result, message).join("\n"); + + return row; + }); return [header, ...body]; }; @@ -140,17 +97,12 @@ type DownloadDialogProps = { open: boolean; setOpen: (open: boolean) => void; message: any; - download_type?: 'answer' | 'all_queries' | 'query'; + download_type?: "answer" | "all_queries" | "query"; }; -export default function DownloadDialog({ - open, - setOpen, - message, - download_type = 'answer', -}: DownloadDialogProps) { - const [type, setType] = React.useState('json'); - const [fileName, setFileName] = React.useState('ROBOKOP_message'); +export default function DownloadDialog({ open, setOpen, message, download_type = "answer" }: DownloadDialogProps) { + const [type, setType] = React.useState("json"); + const [fileName, setFileName] = React.useState(`ROBOKOP_${download_type === "all_queries" ? "query" : "answer"}`); const handleClose = () => { setOpen(false); @@ -158,12 +110,12 @@ export default function DownloadDialog({ const handleClickDownload = async () => { let blob; - if (type === 'json') { - blob = new Blob([JSON.stringify({ message }, null, 2)], { type: 'application/json' }); + if (type === "json") { + blob = new Blob([JSON.stringify({ message }, null, 2)], { type: "application/json" }); } - if (type === 'csv') { + if (type === "csv") { const csvString = await jsonToCsvString(constructCsvObj(message)); - blob = new Blob([csvString], { type: 'text/csv' }); + blob = new Blob([csvString], { type: "text/csv" }); } if (!blob) { @@ -171,7 +123,7 @@ export default function DownloadDialog({ handleClose(); return; } - const a = document.createElement('a'); + const a = document.createElement("a"); a.download = `${fileName}.${type}`; a.href = window.URL.createObjectURL(blob); document.body.appendChild(a); @@ -182,15 +134,13 @@ export default function DownloadDialog({ return ( - Download Answer - + Download {download_type === "all_queries" ? "Query" : "Answer"} + { setFileName(e.target.value); @@ -199,7 +149,7 @@ export default function DownloadDialog({ { // Show the radio group only when the download type is answers. - download_type === 'answer' && ( + download_type === "answer" && ( - The CSV download contains a smaller subset of the answer information. To analyze the - complete properties of the answer graphs, consider using JSON. - - )} + {type === "csv" && The CSV download contains a smaller subset of the answer information. To analyze the complete properties of the answer graphs, consider using JSON.} diff --git a/src/pages/answer/leftDrawer/LeftDrawer.tsx b/src/pages/answer/leftDrawer/LeftDrawer.tsx index 6b49707..96735b8 100644 --- a/src/pages/answer/leftDrawer/LeftDrawer.tsx +++ b/src/pages/answer/leftDrawer/LeftDrawer.tsx @@ -149,7 +149,7 @@ export default function LeftDrawer({ onUpload, displayState, updateDisplayState, - + {summarizeWithAIOpen && setSummarizeWithAIOpen(false)} answerStore={answerStore} />} {summarizeTableWithAIOpen && setSummarizeTableWithAIOpen(false)} answerStore={answerStore} />} diff --git a/src/pages/entryPoint/ExampleModal.tsx b/src/pages/entryPoint/ExampleModal.tsx index 3f4faed..609876d 100644 --- a/src/pages/entryPoint/ExampleModal.tsx +++ b/src/pages/entryPoint/ExampleModal.tsx @@ -1,16 +1,13 @@ -import { Modal } from '@mui/material'; -import React from 'react'; -import { QueryTemplate, TemplatesArray } from '../queryBuilder/templatedQueries/types'; +import { Modal } from "@mui/material"; +import React from "react"; +import { QueryTemplate, TemplatesArray } from "../queryBuilder/templatedQueries/types"; // import examples from '../queryBuilder/templatedQueries/templates.json'; -import { parse } from 'yaml'; +import { parse } from "yaml"; -import './EntryPoint.css'; -import { - TemplateNodePart, - TemplatePart, -} from '../queryBuilder/templatedQueries/TemplateQueriesModal'; -import { useQueryBuilderContext } from '../../context/queryBuilder'; -import { useNavigate } from '@tanstack/react-router'; +import "./EntryPoint.css"; +import { TemplateNodePart, TemplatePart } from "../queryBuilder/templatedQueries/TemplateQueriesModal"; +import { useQueryBuilderContext } from "../../context/queryBuilder"; +import { useNavigate } from "@tanstack/react-router"; interface ExampleModalProps { isOpen: boolean; onClose: () => void; @@ -33,7 +30,7 @@ function ExampleModal({ isOpen, onClose, onCancel }: ExampleModalProps) { React.useEffect(() => { (async () => { try { - const res = await fetch('/templates.yaml'); + const res = await fetch("/templates.yaml"); const text = await res.text(); const data = parse(text) as TemplatesArray; setExamples(data); @@ -45,12 +42,8 @@ function ExampleModal({ isOpen, onClose, onCancel }: ExampleModalProps) { const queryBuilder = useQueryBuilderContext(); const navigate = useNavigate(); - const [selectedExamples, setSelectedExamples] = React.useState( - {} as QueryTemplate - ); - const exampleQueries = (examples as unknown as TemplatesArray).filter( - (example) => example.type === 'example' - ); + const [selectedExamples, setSelectedExamples] = React.useState({} as QueryTemplate); + const exampleQueries = (examples as unknown as TemplatesArray).filter((example) => example.type === "example"); const onModalClose = () => { setSelectedExamples({} as QueryTemplate); onCancel(); @@ -58,16 +51,14 @@ function ExampleModal({ isOpen, onClose, onCancel }: ExampleModalProps) { }; function exampleToTrapiFormat(example: ExampleTemplate) { - const templateNodes = example.template - .filter((part): part is TemplateNodePart => part.type === 'node') - .reduce((obj, { id }) => ({ ...obj, [id]: { categories: [] } }), {} as Record); + const templateNodes = example.template.filter((part): part is TemplateNodePart => part.type === "node").reduce((obj, { id }) => ({ ...obj, [id]: { categories: [] } }), {} as Record); const structureNodes = Object.entries(example.structure.nodes).reduce( (obj, [id, n]) => ({ ...obj, [id]: { categories: [n.category], name: n.name, ...(n.id && { ids: [n.id] }) }, }), - {} as Record + {} as Record, ); const nodesSortedById = Object.entries({ ...templateNodes, ...structureNodes }) @@ -79,7 +70,7 @@ function ExampleModal({ isOpen, onClose, onCancel }: ExampleModalProps) { ...obj, [id]: { subject: e.subject, object: e.object, predicates: [e.predicate] }, }), - {} as Record + {} as Record, ); return { @@ -93,18 +84,19 @@ function ExampleModal({ isOpen, onClose, onCancel }: ExampleModalProps) { } const handleSelectExample = (example: any) => { + console.log("Selected example:", example); const payload = exampleToTrapiFormat(example as ExampleTemplate); - queryBuilder.dispatch({ type: 'saveGraph', payload }); + queryBuilder.dispatch({ type: "saveGraph", payload }); }; const createTemplateDisplay = (template: TemplatePart[]) => { return ( {template.map((part, i) => { - if (part.type === 'text') { + if (part.type === "text") { return {part.text}; } - if (part.type === 'node') { + if (part.type === "node") { return {part.name}; } return null; @@ -116,49 +108,45 @@ function ExampleModal({ isOpen, onClose, onCancel }: ExampleModalProps) {
Example -

Start with an example

+

Start with an example

-

- Select one of the existing queries from our set of examples provided -

+

Select one of the existing queries from our set of examples provided

{exampleQueries.map((query) => (
{ setSelectedExamples(query); handleSelectExample(query); }} >

{createTemplateDisplay(query.template)}

-

- Short description about the query -

+

Short description about the query

))} -
+
- + {buttonOptions?.map((option, index) => ( ({ setInputValue(newValue); onInputChange?.(newValue); - if (value && newValue !== getOptionLabel(value)) { - onChange?.(null); - } - if (!open && newValue) { setOpen(true); } @@ -410,11 +406,11 @@ export function AsyncAutocomplete({ const handleClear = (event: React.MouseEvent) => { event.stopPropagation(); - onChange?.(null); setInputValue(""); onInputChange?.(""); setAsyncResults({}); setSourceLoadingStates({}); + setOpen(true); }; // Sync input value with prop value only when not actively editing