diff --git a/Makefile b/Makefile index 37ede4b..080e337 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,8 @@ build-rapex-studio: @printf "Building studio based on openapi...\n" @mkdir -p assets @rm -rf frontend && cp -r studio frontend && rm -rf frontend/node_modules - @cp studio/custom/home/rapex.tsx frontend/src/pages/Home/index.tsx + @cp studio/custom/components/rapex/Home.tsx frontend/src/pages/Home/index.tsx + @cp studio/custom/components/rapex/ModelConfig.tsx frontend/src/pages/ModelConfig/index.tsx @cp studio/custom/logo/rapex.png frontend/public/assets/logo-white.png @cp studio/custom/logo/rapex.png frontend/src/assets/logo-white.png @cp studio/custom/logo/rapex.png frontend/public/logo.png diff --git a/studio/custom/home/rapex.tsx b/studio/custom/components/rapex/Home.tsx similarity index 90% rename from studio/custom/home/rapex.tsx rename to studio/custom/components/rapex/Home.tsx index aa592ae..7802be1 100644 --- a/studio/custom/home/rapex.tsx +++ b/studio/custom/components/rapex/Home.tsx @@ -154,20 +154,20 @@ const HomePage: React.FC = () => { { key: 'disease', icon: 'biomedgps-disease', - title: 'Disease', + title: 'Diseases', stat: '45,362', }, { key: 'gene', icon: 'biomedgps-gene', - title: 'Gene', + title: 'Genes', stat: '95,141', description: '', }, { key: 'symptom', icon: 'biomedgps-symptom', - title: 'Symptom', + title: 'Symptoms', stat: '23,100', description: '', }, @@ -181,7 +181,7 @@ const HomePage: React.FC = () => { { key: 'knowledges', icon: 'biomedgps-knowledge', - title: 'Knowledge', + title: 'Knowledges', stat: '12,857,601', description: '', }, @@ -206,34 +206,38 @@ const HomePage: React.FC = () => { const images: ImageItem[] = [ { - src: 'https://github.com/yjcyxky/biomedgps-studio/blob/master/public/assets/chatbot.png?raw=true', - title: 'Demo1: Ask questions with chatbot', - }, - { - src: 'https://github.com/yjcyxky/biomedgps-studio/blob/master/public/assets/disease-similarities.png?raw=true', - title: - 'Demo2: Find similar diseases with your queried disease', - }, - { - src: 'https://github.com/yjcyxky/biomedgps-studio/blob/master/public/assets/drug-targets-genes.png?raw=true', - title: - 'Demo3: Predict drugs and related genes for your queried disease', - }, - { - src: 'https://github.com/yjcyxky/biomedgps-studio/blob/master/public/assets/path.png?raw=true', - title: - 'Demo4: Find potential paths between two nodes', - }, - { - src: 'https://github.com/yjcyxky/biomedgps/blob/dev/studio/public/README/images/step2-predict-page.png?raw=true', - title: - 'Predict Interactions', - }, - { - src: 'https://github.com/yjcyxky/biomedgps/blob/dev/studio/public/README/images/step3-explain.png?raw=true', - title: - 'Explain Your Prediction', + src: 'https://rapex.prophetdb.org/assets/examples/rapex_diagram.png', + title: 'RAPEX Overview', }, + // { + // src: 'https://github.com/yjcyxky/biomedgps-studio/blob/master/public/assets/chatbot.png?raw=true', + // title: 'Demo1: Ask questions with chatbot', + // }, + // { + // src: 'https://github.com/yjcyxky/biomedgps-studio/blob/master/public/assets/disease-similarities.png?raw=true', + // title: + // 'Demo2: Find similar diseases with your queried disease', + // }, + // { + // src: 'https://github.com/yjcyxky/biomedgps-studio/blob/master/public/assets/drug-targets-genes.png?raw=true', + // title: + // 'Demo3: Predict drugs and related genes for your queried disease', + // }, + // { + // src: 'https://github.com/yjcyxky/biomedgps-studio/blob/master/public/assets/path.png?raw=true', + // title: + // 'Demo4: Find potential paths between two nodes', + // }, + // { + // src: 'https://github.com/yjcyxky/biomedgps/blob/dev/studio/public/README/images/step2-predict-page.png?raw=true', + // title: + // 'Predict Interactions', + // }, + // { + // src: 'https://github.com/yjcyxky/biomedgps/blob/dev/studio/public/README/images/step3-explain.png?raw=true', + // title: + // 'Explain Your Prediction', + // }, ]; return ( @@ -241,8 +245,8 @@ const HomePage: React.FC = () => {

RAPEX - Response to Air Pollution EXposure (RAPEX)

-

- Enter a air pollutant, gene/protein, disease, drug or symptom name to find and explain related known knowledges in RAPEX platform. If you want to predict new knowledges, please go to the { history.push('/predict-explain/predict-model'); }}>Predict Diseases/Targets page. Please click the following examples to see the results. +

+ Enter an air pollutant, gene/protein, disease, drug or symptom name to find and explain related known knowledges in RAPEX platform. If you want to predict new knowledges, please go to the { history.push('/predict-explain/predict-model'); }}>Predict Diseases/Targets page. Please click the following examples to see the results.

{ + props.onSelect && props.onSelect(value); + }} + onSearch={(value) => handleSearchNode(props.entityType, value)} + getPopupContainer={(triggerNode: HTMLElement) => { + return triggerNode.parentNode as HTMLElement; + }} + // options={entityOptions} + filterOption={false} + notFoundContent={ + + } + > + {entityOptions && + entityOptions.map((option: any) => ( + + {option.metadata ? ( + document.body} + overlayClassName="entity-id-popover" + autoAdjustOverflow={false} + destroyTooltipOnHide={true} + zIndex={1500} + > + {truncateString(option.label, 50)} + + ) : ( + option.label + )} + + ))} + ; +} + +type ModelParameter = { + key: string; + name: string; + type: string; + description: string; + required: boolean; + defaultValue?: any; + entityType?: string; + options?: any[]; + allowMultiple?: boolean; +} + +type ModelItem = { + shortName: string; + name: string; + icon: React.ReactNode; + description: string; + parameters: ModelParameter[]; + handler?: (params: any) => Promise<{ + params: any; + data: GraphData; + }>; + disabled?: boolean; +} + +const ModelConfig: React.FC = (props) => { + const leftSpan = 6; + const [form] = Form.useForm(); + const predictionType = Form.useWatch('prediction_type', form); + + const [loading, setLoading] = useState(false); + const [currentModel, setCurrentModel] = useState(0); + const [params, setParams] = useState({}); + const [graphData, setGraphData] = useState({ nodes: [], edges: [] }); + const [edgeDataSources, setEdgeDataSources] = useState([]); + const [nodeDataSources, setNodeDataSources] = useState([]); + const [relationTypeOptions, setRelationTypeOptions] = useState([]); + const [relationStat, setRelationStat] = useState([]); + + useEffect(() => { + console.log("isAuthenticated in ModelConfig: ", isAuthenticated()); + if (!isAuthenticated()) { + logoutWithRedirect(); + } else { + fetchStatistics().then((data) => { + const relationStats = data.relation_stat; + setRelationStat(relationStats); + + const relationTypes = makeRelationTypes(relationStats); + setRelationTypeOptions(relationTypes); + }); + } + }, []); + + useEffect(() => { + const entityType = form.getFieldValue('entity_type'); + const defaultRelationType = getDefaultRelationType(entityType, predictionType); + form.setFieldsValue({ relation_type: defaultRelationType }); + + // Reset the entity_id field when the prediction type is changed, because the change of prediction type may lead to the component of entity_id missing. + form.setFieldsValue({ entity_id: undefined }); + }, [predictionType]); + + const formatScore = (score: number) => { + // Keep 3 decimal places + return parseFloat(score.toFixed(3)); + } + + useEffect(() => { + if (graphData && graphData.edges) { + const data = makeDataSources(graphData.edges).map((edge) => { + return { + ...edge, + score: formatScore(edge.score) + } + }); + setEdgeDataSources(sortBy(data, ['score']).reverse()); + } + + if (graphData && graphData.nodes) { + setNodeDataSources(makeDataSources(graphData.nodes)); + } + }, [graphData]); + + useEffect(() => { + cleanup(); + }, [currentModel]); + + const cleanup = () => { + form.resetFields(); + setParams({}); + setGraphData({ nodes: [], edges: [] }); + cleanTable() + } + + const cleanTable = () => { + setEdgeDataSources([]); + setNodeDataSources([]); + } + + const [models, setModels] = useState([{ + shortName: 'Disease', + name: 'Prediction for Disease', + icon: , + description: 'To find TopK similar diseases, compounds or targets with a given disease', + parameters: [{ + key: 'prediction_type', + name: 'Prediction Type', + type: 'select', + description: 'Select a type for predicting the result, e.g. SimilarDisease is for predicting similar diseases for a given disease.', + required: true, + options: [ + { label: 'Similar Diseases', value: 'Disease' }, + { label: 'Predicted Compounds', value: 'Compound' }, + { label: 'Predicted Targets', value: 'Gene' } + ], + // defaultValue: 'Disease' + }, + { + key: 'relation_type', + name: 'Relation Type for Prediction', + type: 'RelationTypeSearcher', + description: 'Select a relation type for predicting the result, e.g. Hetionet::DrD::Disease:Disease is for predicting similar diseases for a given disease. The number in the prefix of the relation type is the number of knowledges using to train the model. The larger the number may means the more reliable the prediction.', + required: true, + entityType: 'Disease' + }, + { + key: 'entity_id', + name: 'Disease Name', + type: 'NodeIdSearcher', + description: 'Enter a name of disease for which you want to find similar diseases, compounds or targets. If you find multiple items, you might need to select the most relevant one.', + required: true, + entityType: 'Disease' + }, + // { + // key: 'similarity_score_threshold', + // name: 'Similarity', + // type: 'number', + // description: 'Similarity threshold', + // defaultValue: 0.5, + // required: false + // }, + { + key: 'topk', + name: 'TopK', + type: 'number', + description: 'Number of results to return', + defaultValue: 10, + required: false + }], + handler: (param: any) => { + // const query = { + // operator: 'in', + // value: ["Disease"], + // field: 'entity_type', + // }; + + // const relation_type_map: Record = { + // SimilarDisease: 'Hetionet::DrD::Disease:Disease', + // PredictedDrugs: 'DRUGBANK::treats::Compound:Disease', + // PredictedTargets: 'GNBR::J::Gene:Disease' + // } + + // TODO: Need to update the relation_type automatically + const relation_type = param.relation_type; + + let params: any = { + node_id: `${param.entity_type}${COMPOSED_ENTITY_DELIMITER}${param.entity_id}`, + relation_type: relation_type, + topk: param.topk || 10, + }; + + // TODO: Do we need to add a query string? + // if (query) { + // params['query_str'] = JSON.stringify(query); + // } + + // TODO: How to use similarity_score_threshold? + + return new Promise((resolve, reject) => { + fetchPredictedNodes(params).then((data) => { + console.log('Diseases: ', params, data); + resolve({ + params, + data + }); + }).catch((error) => { + console.log('Diseases Error: ', error); + reject({ nodes: [], edges: [], error: error }) + }); + }); + } + }, + { + shortName: 'Compound', + name: 'Prediction for Compound', + icon: , + description: 'To predict similar compounds, indications or targets for a given drug', + parameters: [{ + key: 'prediction_type', + name: 'Prediction Type', + type: 'select', + description: 'Select a type for predicting the result, e.g. SimilarDrug is for predicting similar compounds for a given drug.', + required: true, + options: [ + { label: 'Similar Compounds', value: 'Compound' }, + { label: 'Predicted Indications', value: 'Disease' }, + { label: 'Predicted Targets', value: 'Gene' } + ], + // defaultValue: 'Compound' + }, + { + key: 'relation_type', + name: 'Relation Type for Prediction', + type: 'RelationTypeSearcher', + description: 'Select a relation type for predicting the result, e.g. DRUGBANK::treats::Compound:Disease is for predicting diseases for a given drug. The number in the prefix of the relation type is the number of knowledges using to train the model. The larger the number may means the more reliable the prediction.', + required: true, + entityType: 'Compound' + }, + { + key: 'entity_id', + name: 'Compound Name', + type: 'NodeIdSearcher', + description: 'Enter a name of drug for which you want to find similar compounds, indications or targets. If you find multiple items, you might need to select the most relevant one.', + required: true, + entityType: 'Compound' + }, + // { + // key: 'score_threshold', + // name: 'Score', + // type: 'number', + // description: 'Score threshold', + // required: false, + // defaultValue: 0.5 + // }, + { + key: 'topk', + name: 'TopK', + type: 'number', + description: 'Number of results to return', + required: false, + defaultValue: 10 + }], + handler: (param: any) => { + // const relation_type_map: Record = { + // SimilarDrug: 'Hetionet::CrC::Compound:Compound', + // PredictedIndications: 'DRUGBANK::treats::Compound:Disease', + // PredictedTargets: 'DRUGBANK::target::Compound:Gene' + // } + + // TODO: Need to update the relation_type automatically + const relation_type = param.relation_type; + + let params: any = { + node_id: `${param.entity_type}${COMPOSED_ENTITY_DELIMITER}${param.entity_id}`, + relation_type: relation_type, + topk: param.topk || 10, + }; + + // TODO: Do we need to add a query string? + // if (query) { + // params['query_str'] = JSON.stringify(query); + // } + + // TODO: How to use similarity_score_threshold? + + return new Promise((resolve, reject) => { + fetchPredictedNodes(params).then((data) => { + console.log('Compounds: ', params, data); + resolve({ + params, + data + }); + }).catch((error) => { + console.log('Compounds Error: ', error); + reject({ nodes: [], edges: [], error: error }) + }); + }); + } + }, + { + shortName: 'Gene', + name: 'Prediction for Gene/Protein', + icon: , + description: 'To predict compounds/diseases for a given gene/protein', + parameters: [{ + key: 'prediction_type', + name: 'Prediction Type', + type: 'select', + description: 'Select a type for predicting the result, e.g. PredictedDrugs is for predicting compounds for a given gene.', + required: true, + options: [ + { label: 'Predicted Compounds', value: 'Compound' }, + { label: 'Predicted Diseases', value: 'Disease' } + ], + // defaultValue: 'Compound' + }, + { + key: 'entity_id', + name: 'Gene/Protein Name', + type: 'NodeIdSearcher', + description: 'Enter a name of gene for which you want to find compounds/diseases. If you find multiple items, you might need to select the most relevant one.', + required: true, + entityType: 'Gene' + }, + { + key: 'relation_type', + name: 'Relation Type for Prediction', + type: 'RelationTypeSearcher', + description: 'Select a relation type for predicting the result, e.g. DRUGBANK::target::Compound:Gene is for predicting compounds for a given gene. The number in the prefix of the relation type is the number of knowledges using to train the model. The larger the number may means the more reliable the prediction.', + required: true, + entityType: 'Gene' + }, + { + key: 'topk', + name: 'TopK', + type: 'number', + description: 'Number of results to return', + required: false, + defaultValue: 10 + }], + handler: (param: any) => { + // const relation_type_map: Record = { + // PredictedDrugs: 'DRUGBANK::target::Compound:Gene', + // PredictedDiseases: 'GNBR::J::Gene:Disease' + // } + + const relation_type = param.relation_type; + + let params: any = { + node_id: `${param.entity_type}${COMPOSED_ENTITY_DELIMITER}${param.entity_id}`, + relation_type: relation_type, + topk: param.topk || 10, + }; + + return new Promise((resolve, reject) => { + fetchPredictedNodes(params).then((data) => { + console.log('Genes: ', params, data); + resolve({ + params, + data + }); + }).catch((error) => { + console.log('Genes Error: ', error); + reject({ nodes: [], edges: [], error: error }) + }); + }); + } + }, + { + shortName: 'Symptom', + name: 'Prediction for Symptom', + icon: , + description: 'To predict compounds for a given group of symptoms', + parameters: [{ + key: 'prediction_type', + name: 'Prediction Type', + type: 'select', + description: 'Select a type for predicting the result, e.g. Disease is for predicting diseases for a given symptom.', + required: true, + options: [ + { label: 'Predicted Compounds', value: 'Compound' }, + { label: 'Predicted Diseases', value: 'Disease' }, + { label: 'Predicted Genes', value: 'Gene' } + ], + // defaultValue: 'Disease' + }, + { + key: 'relation_type', + name: 'Relation Type for Prediction', + type: 'RelationTypeSearcher', + description: 'Select a relation type for predicting the result, e.g. HSDN::has_symptom::Disease:Symptom is for predicting diseases for a given symptom. The number in the prefix of the relation type is the number of knowledges using to train the model. The larger the number may means the more reliable the prediction.', + required: true, + entityType: 'Symptom' + }, + { + key: 'entity_id', + name: 'Symptom Name', + type: 'NodeIdSearcher', + description: 'Enter a name of symptom for which you want to find similar compounds. If you find multiple items, you might need to select the most relevant one or select multiple items.', + required: true, + entityType: 'Symptom', + allowMultiple: true + }, + { + key: 'topk', + name: 'TopK', + type: 'number', + description: 'Number of results to return', + required: false, + defaultValue: 10 + }], + handler: (param: any) => { + console.log('Symptoms Parameters: ', param) + const relation_type = param.relation_type; + + let node_id = ''; + if (param.entity_id.length > 1) { + let node_ids = []; + for (let i = 0; i < param.entity_id.length; i++) { + node_ids.push(`${param.entity_type}${COMPOSED_ENTITY_DELIMITER}${param.entity_id[i]}`); + } + + console.log("node_ids: ", node_ids) + node_id = node_ids.join(','); + } else { + node_id = `${param.entity_type}${COMPOSED_ENTITY_DELIMITER}${param.entity_id}`; + } + + let params: any = { + node_id: node_id, + relation_type: relation_type, + topk: param.topk || 10, + }; + + return new Promise((resolve, reject) => { + fetchPredictedNodes(params).then((data) => { + console.log('Symptoms: ', params, data); + resolve({ + params, + data + }); + }).catch((error) => { + console.log('Symptoms Error: ', error); + reject({ nodes: [], edges: [], error: error }) + }); + }); + }, + }, + { + shortName: 'MOA', + name: 'Predicted MOAs', + icon: , + description: 'To predict MOAs for a given drug and disease', + parameters: [{ + key: 'entity_id', + name: 'Disease', + type: 'NodeIdSearcher', + description: 'Enter a name of disease for which you want to find mode of actions', + required: true, + entityType: 'Disease' + }, { + key: 'entity_id', + name: 'Compound', + type: 'NodeIdSearcher', + description: 'Enter a name of drug for which you want to find mode of actions', + required: true, + entityType: 'Compound' + }, { + key: 'topk', + name: 'TopK', + type: 'number', + description: 'Number of results to return', + required: false, + defaultValue: 10 + }], + disabled: true + }]) + + const handleMenuClick = (e: any) => { + console.log('handleMenuClick: ', e); + if (models[e.key]) { + setCurrentModel(e.key); + } + }; + + // TODO: Need to change the default relation types if we changed the relation types in our database. + const getDefaultRelationType = (entityType: string, predictionType: string) => { + const DefaultRelationTypeMap: Record = { + 'Disease:Disease': 'BioMedGPS::SimilarWith::Disease:Disease', + 'Compound:Compound': 'BioMedGPS::SimilarWith::Compound:Compound', + 'Disease:Compound': 'BioMedGPS::Treatment::Compound:Disease', + 'Disease:Gene': 'BioMedGPS::Causer::Gene:Disease', + 'Compound:Disease': 'BioMedGPS::Treatment::Compound:Disease', + 'Compound:Gene': 'BioMedGPS::Target::Gene:Compound', + 'Gene:Compound': 'BioMedGPS::Target::Gene:Compound', + 'Gene:Disease': 'BioMedGPS::Causer::Gene:Disease', + // TODO: the relation type is non-standard + 'Symptom:Disease': 'BioMedGPS::Present::Disease:Symptom', + 'Symptom:Compound': 'BioMedGPS::Treatment::Compound:Symptom', + 'Symptom:Gene': 'BioMedGPS::Causer::Gene:Symptom', + }; + + const entityPair = `${entityType}:${predictionType}`; + return DefaultRelationTypeMap[entityPair] + } + + + const detectComponent = (item: ModelParameter, onChange: (value: any) => void): React.ReactNode => { + if (item.type === 'NodeIdSearcher') { + return { + onChange(value); + }} + allowMultiple={item.allowMultiple} + handleSearchNode={(entityType, value) => console.log(entityType, value)} + // @ts-ignore + getEntities={fetchEntities} + /> + } else if (item.type === 'RelationTypeSearcher') { + console.log("RelationTypeSearcher: ", item, relationTypeOptions, form.getFieldValue('entity_type'), predictionType); + + // TODO: Need to improve the regex to match the standard format of relation type. + let filteredRelationTypeOptions = relationTypeOptions.filter((option) => { + return item.entityType ? option.value.indexOf(item.entityType) !== -1 && option.value.match(/[a-zA-Z\+_\-]+::[a-zA-Z\+_\-]+::?[a-zA-Z]+:[a-zA-Z]+/g) : true; + }); + + let defaultRelationType = undefined; + if (predictionType) { + if (predictionType === item.entityType) { + filteredRelationTypeOptions = filteredRelationTypeOptions.filter((option) => { + return option.value.indexOf(`${predictionType}:${predictionType}`) !== -1; + }) + } else { + filteredRelationTypeOptions = filteredRelationTypeOptions.filter((option) => { + return option.value.indexOf(predictionType) !== -1; + }) + } + + if (item.entityType) { + defaultRelationType = getDefaultRelationType(item.entityType, predictionType) || filteredRelationTypeOptions[0]?.value; + } + }; + + return + } else if (item.type === 'number') { + return onChange(value)} + placeholder={item.description} + min={1} + max={500} + /> + } else if (item.type === 'select') { + return onChange(event.target.value)} + placeholder={item.description} + /> + } + } + + const renderForm = () => { + const parameters = models[currentModel].parameters; + const entityIdIndex = parameters.findIndex((param) => param.key === 'entity_id'); + + const formItems = models[currentModel].parameters.map((param, index) => { + return ( + + {detectComponent(param, (value) => { + form.setFieldValue(param.key, value); + if (param.entityType) { + form.setFieldValue('entity_type', param.entityType); + } + console.log("onSelect: ", param.key, value, form.getFieldsValue(), form.getFieldValue('entity_type')); + })} + + ); + }); + + if (entityIdIndex !== -1) { + const param = parameters[entityIdIndex]; + // Add entity type field into the formItems array where the position is entityIdIndex + 1 + formItems.splice(entityIdIndex, 0, ); + } + + return formItems; + }; + + // Placeholder function for submitting the form + const handleSubmit = () => { + // We need to clean the table before we submit the form, otherwise, the table will show the previous result. + cleanTable(); + setLoading(true); + form + .validateFields() + .then((values) => { + const updatedValues = form.getFieldsValue(); + console.log('ModelConfig - onConfirm: ', values, updatedValues); + + const model = models[currentModel]; + if (model && model.handler) { + model.handler(updatedValues).then((resp) => { + const { params, data } = resp; + console.log('ModelConfig - onConfirm - handler: ', params, data); + setParams(params); + setGraphData(data); + }).catch((error) => { + console.log('ModelConfig - onConfirm - handler - Error: ', error); + message.warning("Cannot find any result for the given parameters.", 5) + setParams({}); + setGraphData(error); + }).finally(() => { + setLoading(false); + }); + } else { + setLoading(false); + } + }) + .catch((error) => { + console.log('onConfirm Error: ', error); + setLoading(false); + }); + }; + + const detectColor = (modelName: string) => { + if (modelName === models[currentModel].name) { + return '#000000d9'; + } else { + return '#999'; + } + } + + return ( + isAuthenticated() && + + + {models.map((model, index) => ( + + ${model.name} | ${model.description}`} placement="right" key={index}> + + + {model.shortName} + + ))} + + + + +
+

{models[currentModel].name}

+

{models[currentModel].description}

+
+
+ {renderForm()} + +
+ + + {loading ? + + + : + { + setLoading(true); + const source_id = row.source_id; + const source_type = row.source_type; + const target_id = row.target_id; + const target_type = row.target_type; + const relation_type = row.reltype; + + const first_fn = fetchOneStepLinkedNodes({ + query_str: makeQueryStr(source_type, source_id), + page_size: 40 + }) + + const second_fn = fetchOneStepLinkedNodes({ + query_str: makeQueryStr(target_type, target_id), + page_size: 40 + }) + + Promise.all([first_fn, second_fn]).then((responses) => { + const source_nodes = responses[0]; + const target_nodes = responses[1]; + + let d = { + nodes: source_nodes.nodes.concat(target_nodes.nodes) as GraphNode[], + edges: source_nodes.edges.concat(target_nodes.edges) as GraphEdge[] + } + + const edges = edgeDataSources + .filter((edge) => row.relid === edge.relid) + .map((edge) => edge.metadata); + + d = { + nodes: d.nodes, + edges: d.edges.concat(edges as GraphEdge[]) + } + + console.log('ExplainRow: ', row, d, source_nodes, target_nodes, edges); + setLoading(false); + if (d && d.nodes && d.nodes.length > 0) { + pushGraphDataToLocalStorage(d); + history.push('/predict-explain/knowledge-graph'); + } else { + message.warning("Cannot find an attention subgraph for explaining the predicted relation.", 5) + } + }).catch((error) => { + setLoading(false); + console.log('ExplainRow Error: ', error); + message.warning("Cannot find an attention subgraph for explaining the predicted relation.", 5) + }); + }} + onLoadGraph={(graph) => { + console.log('onLoadGraph: ', graph); + if (graph && graph.nodes && graph.nodes.length > 0) { + pushGraphDataToLocalStorage(graph); + history.push('/predict-explain/knowledge-graph'); + } else { + message.warning("You need to generate some predicted result and pick up the interested rows first.", 5) + } + }} + edgeStat={relationStat} + /> + } + +
+
+ ); +}; + +export default ModelConfig; diff --git a/studio/custom/route/rapex.ts b/studio/custom/route/rapex.ts index 29a765c..2dcb745 100644 --- a/studio/custom/route/rapex.ts +++ b/studio/custom/route/rapex.ts @@ -62,35 +62,35 @@ export const routes = [ }, { path: '/knowledge-graph-editor', - name: 'knowledge-graph-editor', + name: 'Knowledge Graph Editor', icon: 'link', hideInMenu: true, component: './KnowledgeGraphEditor', category: 'knowledge-graph' }, { - name: 'chatbot', + name: 'Chatbot', icon: 'comment', path: '/chatbot', hideInMenu: true, component: './ChatBot', }, { - name: 'about', + name: 'About', icon: 'info-circle', path: '/about', hideInMenu: true, component: './About', }, { - name: 'help', + name: 'Help', icon: 'QuestionCircleOutlined', path: '/help', // hideInMenu: true, component: './Help', }, { - name: 'changelog', + name: 'ChangeLog', icon: 'field-time', path: '/changelog', hideInMenu: true, diff --git a/studio/package.json b/studio/package.json index 0727ef7..682cede 100644 --- a/studio/package.json +++ b/studio/package.json @@ -9,7 +9,7 @@ "build": "max build", "build:embed": "cross-env UMI_APP_IS_STATIC=true UMI_ENV=embed max build", "build:biomedgps-embed": "cross-env UMI_APP_IS_STATIC=true UMI_ENV=embed UMI_APP_AUTH0_CLIENT_ID=Y08FauV1dAEiocNIZt5LiOifzNgXr6Uo UMI_APP_AUTH0_DOMAIN=biomedgps.jp.auth0.com max build", - "build:rapex-embed": "cross-env UMI_APP_IS_STATIC=true UMI_ENV=embed max build", + "build:rapex-embed": "cross-env UMI_APP_IS_STATIC=true UMI_ENV=embed UMI_APP_HEADER_HIDDEN=true max build", "build:analyze": "cross-env ANALYZE=true max build", "format": "prettier --cache --write .", "postinstall": "max setup", @@ -17,7 +17,7 @@ "start": "npm run dev", "start:local-dev": "cross-env UMI_APP_API_PREFIX=http://localhost:3000 max dev", "start:biomedgps-remote-dev": "cross-env UMI_APP_API_PREFIX=https://drugs.3steps.cn UMI_APP_AUTH0_CLIENT_ID=Y08FauV1dAEiocNIZt5LiOifzNgXr6Uo UMI_APP_AUTH0_DOMAIN=biomedgps.jp.auth0.com max dev", - "start:rapex-remote-dev": "cross-env UMI_APP_API_PREFIX=https://rapex.prophetdb.org max dev", + "start:rapex-remote-dev": "cross-env UMI_APP_API_PREFIX=https://rapex.prophetdb.org UMI_APP_HEADER_HIDDEN=true max dev", "openapi": "max openapi" }, "dependencies": { diff --git a/studio/public/examples/rapex_diagram.png b/studio/public/examples/rapex_diagram.png new file mode 100644 index 0000000..55ce8a4 Binary files /dev/null and b/studio/public/examples/rapex_diagram.png differ diff --git a/studio/src/NodeInfoPanel/Components/GTexViewer/Components/GTexGeneViolinViewer/index.tsx b/studio/src/NodeInfoPanel/Components/GTexViewer/Components/GTexGeneViolinViewer/index.tsx index 2e08a48..8862df3 100644 --- a/studio/src/NodeInfoPanel/Components/GTexViewer/Components/GTexGeneViolinViewer/index.tsx +++ b/studio/src/NodeInfoPanel/Components/GTexViewer/Components/GTexGeneViolinViewer/index.tsx @@ -115,7 +115,7 @@ const GTexGeneViolinViewer: React.FC = (props) => {
diff --git a/studio/src/NodeInfoPanel/CompoundInfoPanel/index.less b/studio/src/NodeInfoPanel/CompoundInfoPanel/index.less index 55370fa..c7470bb 100644 --- a/studio/src/NodeInfoPanel/CompoundInfoPanel/index.less +++ b/studio/src/NodeInfoPanel/CompoundInfoPanel/index.less @@ -20,7 +20,7 @@ } .general-information .ant-descriptions-title { - font-size: 1.5em; + font-size: 1.2rem; } .biology-background { @@ -29,7 +29,7 @@ .section { h2 { - font-size: 1.5em; + font-size: 1.2rem; } } } diff --git a/studio/src/NodeInfoPanel/ProteinInfoPanel/index.less b/studio/src/NodeInfoPanel/ProteinInfoPanel/index.less index abfc019..4d70064 100644 --- a/studio/src/NodeInfoPanel/ProteinInfoPanel/index.less +++ b/studio/src/NodeInfoPanel/ProteinInfoPanel/index.less @@ -8,7 +8,7 @@ } .general-information .ant-descriptions-title { - font-size: 1.5em; + font-size: 1.2rem; } .biology-background { diff --git a/studio/src/NodeInfoPanel/ProteinInfoPanel/index.tsx b/studio/src/NodeInfoPanel/ProteinInfoPanel/index.tsx index dd5b71d..7c57cb9 100644 --- a/studio/src/NodeInfoPanel/ProteinInfoPanel/index.tsx +++ b/studio/src/NodeInfoPanel/ProteinInfoPanel/index.tsx @@ -181,7 +181,7 @@ const ComposedProteinPanel: React.FC = (props) => { }); oItems.push({ // @ts-ignore, we don't care about the warning. We need it to be a Tag component. - label: Alignment, + label: Alignment, key: oItems.length + 1, children: }) diff --git a/studio/src/app.tsx b/studio/src/app.tsx index 159cfd9..8aadc0a 100644 --- a/studio/src/app.tsx +++ b/studio/src/app.tsx @@ -1,5 +1,6 @@ import Footer from '@/components/Footer'; import Header from '@/components/Header'; +import { ConfigProvider } from 'antd'; import { RequestConfig, history, RuntimeConfig, request as UmiRequest } from 'umi'; import { PageLoading, SettingDrawer } from '@ant-design/pro-components'; import { Auth0Provider } from '@auth0/auth0-react'; @@ -142,8 +143,16 @@ export async function getInitialState(): Promise<{ } export function rootContainer(container: React.ReactNode): React.ReactNode { + const component = + {container} + ; + if (!isAuthEnabled()) { - return container; + return component; } return ( @@ -153,7 +162,7 @@ export function rootContainer(container: React.ReactNode): React.ReactNode { authorizationParams={{ redirect_uri: window.location.origin }}> - {container} + {component} ); }; diff --git a/studio/src/components/Footer/index.tsx b/studio/src/components/Footer/index.tsx index f0d1b67..f706530 100644 --- a/studio/src/components/Footer/index.tsx +++ b/studio/src/components/Footer/index.tsx @@ -67,7 +67,7 @@ const Footer: React.FC = () => { cookieName={cookieName} style={{ background: '#2B373B' }} enableDeclineButton - buttonStyle={{ color: '#4e503b', fontSize: '13px' }} + buttonStyle={{ color: '#4e503b', fontSize: '0.9rem' }} expires={150} onAccept={() => { allowTrack(); diff --git a/studio/src/components/Header/index.tsx b/studio/src/components/Header/index.tsx index afb25d4..bde0588 100644 --- a/studio/src/components/Header/index.tsx +++ b/studio/src/components/Header/index.tsx @@ -1,7 +1,7 @@ import { QuestionCircleOutlined, InfoCircleOutlined, UserOutlined, FieldTimeOutlined, LogoutOutlined } from '@ant-design/icons'; import { Space, Menu, Button, message, Dropdown } from 'antd'; import React, { useEffect, useState } from 'react'; -import { getJwtAccessToken, logoutWithRedirect, isAuthEnabled } from '@/components/util'; +import { getJwtAccessToken, logoutWithRedirect, isAuthEnabled, isHeaderHidden } from '@/components/util'; import { useAuth0 } from "@auth0/auth0-react"; import type { MenuProps } from 'antd'; import { history } from 'umi'; @@ -155,10 +155,16 @@ const GlobalHeaderRight: React.FC = (props) => { return ( - - - - + { + isHeaderHidden() ? null : ( + + + + + + + ) + } { isAuthEnabled() && !isAuthenticated ? ( diff --git a/studio/src/components/util.ts b/studio/src/components/util.ts index 9165548..dcc4e55 100644 --- a/studio/src/components/util.ts +++ b/studio/src/components/util.ts @@ -162,3 +162,8 @@ export const getUsername = (): string | undefined => { return undefined; } } + +// Environmental Variables +export const isHeaderHidden = () => { + return process.env.UMI_APP_HIDE_HEADER ? true : false +} \ No newline at end of file diff --git a/studio/src/global.less b/studio/src/global.less index 8073a97..4313e02 100644 --- a/studio/src/global.less +++ b/studio/src/global.less @@ -5,6 +5,7 @@ body, #root { height: 100%; min-width: 1000px; + font-size: 18px; } .colorWeak { diff --git a/studio/src/pages/Home/index.tsx b/studio/src/pages/Home/index.tsx index 0da235b..7ce1766 100644 --- a/studio/src/pages/Home/index.tsx +++ b/studio/src/pages/Home/index.tsx @@ -233,7 +233,7 @@ const HomePage: React.FC = () => { -

+

Enter a gene/protein, disease, drug or symptom name to find and explain related known knowledges in our platform.
If you want to predict new knowledges, please go to the { history.push('/predict-explain/predict-model'); }}>Predict Drug/Target page. diff --git a/studio/src/pages/ModelConfig/index.less b/studio/src/pages/ModelConfig/index.less index 383aaaa..3c22c37 100644 --- a/studio/src/pages/ModelConfig/index.less +++ b/studio/src/pages/ModelConfig/index.less @@ -15,7 +15,7 @@ .ant-menu-item { height: 100px; - padding: 16px !important; + padding: 6px !important; .ant-menu-title-content { display: flex; @@ -98,7 +98,7 @@ .ant-tabs-extra-content { .ant-btn { font-weight: bold; - font-size: 16px; + font-size: 1rem; height: auto; background-color: #ff1818; border-color: #ff1818;