Skip to content

Commit

Permalink
Add the EdgeInfoPanel for each knowledge.
Browse files Browse the repository at this point in the history
  • Loading branch information
yjcyxky committed Mar 24, 2024
1 parent e9c54ec commit 28942b0
Show file tree
Hide file tree
Showing 30 changed files with 625 additions and 73 deletions.
2 changes: 1 addition & 1 deletion studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"antd": "5.8.0",
"antd-schema-form": "^4.5.1",
"axios": "^1.1.2",
"biominer-components": "0.3.17",
"biominer-components": "0.3.18",
"classnames": "^2.3.0",
"handlebars": "^4.7.7",
"handsontable": "^12.1.3",
Expand Down
34 changes: 34 additions & 0 deletions studio/src/EdgeInfoPanel/CommonPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { useEffect } from 'react';
import { Empty, Tabs } from 'antd';
import type { EdgeInfo } from './index.t';

import './index.less';

type CommonPanelProps = {
edgeInfo?: EdgeInfo;
children?: React.ReactNode;
};

const CommonPanel: React.FC<CommonPanelProps> = (props) => {
const { edge, startNode, endNode } = props.edgeInfo || {
edge: undefined,
startNode: undefined,
endNode: undefined,
};

useEffect(() => { }, [edge, startNode, endNode]);

return (
<Tabs className="common-info-panel">
{
props.children ? (
<Tabs.TabPane tab={'Summary'} key={'summary'}>
{props.children}
</Tabs.TabPane>
) : <Empty />
}
</Tabs>
);
};

export default CommonPanel;
46 changes: 46 additions & 0 deletions studio/src/EdgeInfoPanel/DiseaseGenePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useEffect } from 'react';
import { Tabs } from 'antd';
import type { EdgeInfo } from './index.t';

import './index.less';

type DiseaseGenePanelProps = {
edgeInfo?: EdgeInfo;
children?: React.ReactNode;
};

const DiseaseGenePanel: React.FC<DiseaseGenePanelProps> = (props) => {
const { edge, startNode, endNode } = props.edgeInfo || {
edge: undefined,
startNode: undefined,
endNode: undefined,
};

useEffect(() => {
console.log('GeneDiseasePanel: ', edge, startNode, endNode);
}, [edge, startNode, endNode]);

return (
<Tabs className="gene-disease-info-panel tabs-nav-right">
{props.children ? (
<Tabs.TabPane tab={'Summary'} key={'summary'}>
{props.children}
</Tabs.TabPane>
) : null}
<Tabs.TabPane tab={'GeneDiease Info'} key={'gene-disease-info'}>
We can show the gene-disease association information here. Maybe it's summarized information
from publications.
</Tabs.TabPane>
<Tabs.TabPane tab={'Diff Expression'} key={'diff-expr'}>
we can show the diff expression here. It can tell us whether the gene is up-regulated or
down-regulated in the disease.
</Tabs.TabPane>
<Tabs.TabPane tab={'Biomarkers'} key={'biomarker'}>
we can show the related biomarkers. It can tell us which genes are the biomarkers of the
disease.
</Tabs.TabPane>
</Tabs>
);
};

export default DiseaseGenePanel;
44 changes: 44 additions & 0 deletions studio/src/EdgeInfoPanel/DrugDiseasePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useEffect } from 'react';
import { Tabs } from 'antd';
import type { EdgeInfo } from './index.t';

import './index.less';

type DrugDiseasePanel = {
edgeInfo?: EdgeInfo;
children?: React.ReactNode;
};

const DrugDiseasePanel: React.FC<DrugDiseasePanel> = (props) => {
const { edge, startNode, endNode } = props.edgeInfo || {
edge: undefined,
startNode: undefined,
endNode: undefined,
};

useEffect(() => { }, [edge, startNode, endNode]);

return (
<Tabs className="drug-disease-info-panel tabs-nav-right">
{props.children ? (
<Tabs.TabPane tab={'Summary'} key={'summary'}>
{props.children}
</Tabs.TabPane>
) : null}
<Tabs.TabPane tab={'DrugDisease Info'} key={'drug-disease-info'}>
We can show the drug-disease association information here. Maybe it's summarized information
from clinical trials, or publications.
</Tabs.TabPane>
<Tabs.TabPane tab={'Patents'} key={'drug-patent-info'}>
We can show the patents information here. Maybe it's summarized information from patents
database.
</Tabs.TabPane>
<Tabs.TabPane tab={'Products'} key={'drug-product-info'}>
We can show the production information here. Maybe it's summarized information from drug
production database.
</Tabs.TabPane>
</Tabs>
);
};

export default DrugDiseasePanel;
39 changes: 39 additions & 0 deletions studio/src/EdgeInfoPanel/DrugGenePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useEffect } from 'react';
import { Tabs } from 'antd';
import type { EdgeInfo } from './index.t';

import './index.less';

type DrugGenePanelProps = {
edgeInfo?: EdgeInfo;
children?: React.ReactNode;
};

const DrugGenePanel: React.FC<DrugGenePanelProps> = (props) => {
const { edge, startNode, endNode } = props.edgeInfo || {
edge: undefined,
startNode: undefined,
endNode: undefined,
};

useEffect(() => { }, [edge, startNode, endNode]);

return (
<Tabs className="drug-gene-info-panel">
{props.children ? (
<Tabs.TabPane tab={'Summary'} key={'summary'}>
{props.children}
</Tabs.TabPane>
) : null}
<Tabs.TabPane tab={'DrugGene Info'} key={'drug-gene-info'}>
We can show the drug-gene association information here. Maybe it's summarized information
from publications.
</Tabs.TabPane>
<Tabs.TabPane tab={'Drug Targets'} key={'clinical-trails'}>
Comming soon...
</Tabs.TabPane>
</Tabs>
);
};

export default DrugGenePanel;
76 changes: 76 additions & 0 deletions studio/src/EdgeInfoPanel/PublicationDesc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useState } from 'react';
import { Button } from 'antd';
import parse from 'html-react-parser';
import type { PublicationDetail } from 'biominer-components/dist/typings';

export const SEPARATOR = '#';

const Desc: React.FC<{
publication: PublicationDetail,
showAbstract: (doc_id: string) => Promise<PublicationDetail>,
showPublication: (publication: PublicationDetail) => void,
queryStr: string
}> = (props) => {
const { publication } = props;
const [abstract, setAbstract] = useState<string>('');
const [abstractVisible, setAbstractVisible] = useState<boolean>(false);

const fetchAbstract = (doc_id: string) => {
props.showAbstract(doc_id).then((publication) => {
console.log('fetchAbstract for a publication: ', publication);
setAbstract(publication.article_abstract || '');
setAbstractVisible(true);
}).catch((error) => {
console.error('Error: ', error);
setAbstract('');
setAbstractVisible(false);
});
};

const escapeRegExp = (str: string) => {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

const highlightWords = (text: string, words: string[]): string => {
let newText = text;
words.forEach(word => {
let escapedWord = escapeRegExp(word);
let regex = new RegExp(`(${escapedWord})(?![^<]*>|[^<>]*<\/)`, 'gi');
newText = newText.replace(regex, '<span class="highlight">$1</span>');
});

return newText;
}

return (
<div>
<p>
{parse(highlightWords(publication.summary, props.queryStr.split(SEPARATOR)))}
<Button type="link" onClick={() => {
if (abstractVisible) {
setAbstractVisible(false);
} else {
fetchAbstract(publication.doc_id);
}
}} style={{ paddingLeft: '2px' }}>
{abstractVisible ? 'Hide Abstract' : 'Show Abstract'}
</Button>
</p>
{
abstractVisible ?
<p>{parse(highlightWords(abstract, props.queryStr.split(SEPARATOR)))}</p> : null
}
<p>
{publication.year} | {publication.journal} &nbsp; | &nbsp; {publication.authors ? publication.authors.join(', ') : 'Unknown'}
</p>
{
<p>
Cited by {publication.citation_count ? publication.citation_count : 0} publications &nbsp; | &nbsp;
<a onClick={(e) => { props.showPublication(publication) }}>View Publication</a>
</p>
}
</div>
);
};

export default Desc;
136 changes: 136 additions & 0 deletions studio/src/EdgeInfoPanel/PublicationPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React, { useEffect, useState } from 'react';
import { Button, List, message } from 'antd';
import { FileProtectOutlined } from '@ant-design/icons';
import type { Publication, PublicationDetail } from 'biominer-components/dist/typings';
import PublicationDesc from './PublicationDesc';
import { fetchPublication, fetchPublications } from '@/services/swagger/KnowledgeGraph';

import './index.less';

export type PublicationPanelProps = {
queryStr: string;
};

const PublicationPanel: React.FC<PublicationPanelProps> = (props) => {
const [publications, setPublications] = useState<Publication[]>([]);
const [page, setPage] = useState<number>(0);
const [total, setTotal] = useState<number>(0);
const [pageSize, setPageSize] = useState<number>(10);
const [loading, setLoading] = useState<boolean>(false);
const [publicationMap, setPublicationMap] = useState<Record<string, PublicationDetail>>({});

const showAbstract = (doc_id: string): Promise<PublicationDetail> => {
console.log('Show Abstract: ', doc_id);
return new Promise((resolve, reject) => {
fetchPublication({ id: doc_id }).then((publication) => {
console.log('Publication: ', publication);
setPublicationMap({
...publicationMap,
[doc_id]: publication
})
resolve(publication);
}).catch((error) => {
console.error('Error: ', error);
reject(error);
});
});
};

useEffect(() => {
if (!props.queryStr) {
return;
}

setLoading(true);
fetchPublications(
{
query_str: props.queryStr,
page: 0,
page_size: 10
}).then((publications) => {
setPublications(publications.records);
setPage(publications.page);
setTotal(publications.total);
setPageSize(publications.page_size);
}).catch((error) => {
console.error('Error: ', error);
message.error('Failed to fetch publications');
}).finally(() => {
setLoading(false);
});
}, [props.queryStr, page, pageSize]);

const showPublication = async (publication: PublicationDetail) => {
console.log('Show Publication: ', publication);
if (publication) {
console.log('Publication Map: ', publicationMap);
const link = publication?.provider_url;
const doi_link = "https://doi.org/" + publication?.doi;

if (publication?.doi) {
window.open(doi_link, '_blank');
} else if (link) {
window.open(link, '_blank');
} else {
message.warning('No link available for this publication');
}
}
};

const onClickPublication = (item: Publication) => {
if (publicationMap[item.doc_id]) {
showPublication(publicationMap[item.doc_id])
} else {
showAbstract(item.doc_id).then((publication) => {
showPublication(publication);
}).catch((error) => {
message.error('Failed to fetch publication details');
});
}
}

return (
<>
<div className='publication-panel-header'>
<h3>
Relevant Publications
</h3>
<span>Keywords: {props.queryStr.split('#').join(', ')}</span>
</div>
<List
loading={loading}
itemLayout="horizontal"
rowKey={'doc_id'}
dataSource={publications}
size="large"
pagination={{
disabled: true,
position: 'top',
current: page,
total: total,
pageSize: pageSize,
onChange: (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
}
}}
renderItem={(item, index) => (
<List.Item>
<List.Item.Meta
avatar={<FileProtectOutlined />}
title={<a onClick={(e) => { onClickPublication(item); }}>{item.title}</a>}
description={
<PublicationDesc publication={item}
showAbstract={showAbstract} queryStr={props.queryStr}
showPublication={(publication) => onClickPublication(publication)}
/>
}
/>
</List.Item>
)}
/>
</>
);
};

export default PublicationPanel;
Loading

0 comments on commit 28942b0

Please sign in to comment.