Skip to content

Commit

Permalink
Merge pull request #6 from the-virtual-brain/dev_LM
Browse files Browse the repository at this point in the history
VT-23. Finalize POC of extension
  • Loading branch information
romina1601 authored Sep 11, 2024
2 parents 473e8c4 + 2c1181f commit 4196844
Show file tree
Hide file tree
Showing 19 changed files with 1,032 additions and 125 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,5 @@ dmypy.json

# Yarn cache
.yarn/
workspace_export.py
test_output
372 changes: 372 additions & 0 deletions notebooks/api-test.ipynb

Large diffs are not rendered by default.

24 changes: 20 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import TreeViewComponent from './components/TreeView';
const App: React.FC = () => {
const [selectedNode, setSelectedNode] = useState<ISelectedNodeType | null>(null);

const [workspace, setWorkspace] = useState<IWorkspaceState>({
const initialWorkspaceState: IWorkspaceState = {
model: null,
connectivity: null,
parcellation: null,
tractogram: null,
coupling: null,
noise: null,
integrationMethod: null
});
};
const [workspace, setWorkspace] = useState<IWorkspaceState>(initialWorkspaceState);

const addToWorkspace = (node: ISelectedNodeType) => {
setWorkspace(prevWorkspace => {
Expand All @@ -35,10 +37,24 @@ const App: React.FC = () => {
});
};

const updateConnectivityOptions = (optionType: 'parcellation' | 'tractogram', value: string) => {
setWorkspace(prev => {
return {
...prev,
parcellation: optionType === 'parcellation' ? value : prev.parcellation,
tractogram: optionType === 'tractogram' ? value : prev.tractogram,
};
});
};

const resetWorkspace = () => {
setWorkspace(initialWorkspaceState);
};

return (
<div className="layout">
<InfoBoxComponent selectedNode={selectedNode} addToWorkspace={addToWorkspace} />
<WorkspaceComponent workspace={workspace} />
<WorkspaceComponent workspace={workspace} updateConnectivityOptions={updateConnectivityOptions} resetWorkspace={resetWorkspace}/>
<TreeViewComponent selectedNode={selectedNode} />
<GraphViewComponent setSelectedNode={setSelectedNode} />
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/GaphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
type: node.type,
definition: node.definition,
iri: node.iri,
requires: node.requires,
childNodes: node.childNodes,
childLinks: node.childLinks,
collapsed: false
Expand Down Expand Up @@ -111,7 +112,7 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
}
};

// for graph centering when it is first rendered
// center graph when it is first rendered
useEffect(() => {
if (fgRef.current && data.nodes.length > 0 && isInitialRender) {
fgRef.current.centerAt(75, 75);
Expand Down
36 changes: 30 additions & 6 deletions src/components/InfoBox.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { ISelectedNodeType } from './interfaces/InfoBoxInterfaces';
import { fetchNodeByLabel } from '../handler';

interface IInfoBoxProps {
selectedNode: {
Expand All @@ -8,23 +9,46 @@ interface IInfoBoxProps {
type: string;
definition: string;
iri: string;
requires: string[];
} | null;
addToWorkspace: (node: ISelectedNodeType) => void;
}

const InfoBoxComponent: React.FC<IInfoBoxProps> = ({
selectedNode,
addToWorkspace
}) => {
const InfoBoxComponent: React.FC<IInfoBoxProps> = ({ selectedNode, addToWorkspace }) => {
// Valid types for adding objects to workspace
// TODO: add valid type for connectivity
const validTypes = ['Neural Mass Model', 'Noise', 'Coupling', 'Integrator'];

const isAddable = selectedNode && validTypes.includes(selectedNode.type);

const handleAddToWorkspace = async () => {
if (!selectedNode) return;

if (isAddable) {
addToWorkspace(selectedNode);

// check requirements of selected node
if (selectedNode.requires && selectedNode.requires.length > 0) {
const requiredNodePromises = selectedNode.requires.map(label => fetchNodeByLabel(label));
const requiredNodesResponses = await Promise.all(requiredNodePromises);

requiredNodesResponses.forEach(response => {
const { nodes } = response; // Extract nodes from the response
nodes.forEach(node => {
if (validTypes.includes(node.type) && (node.type !== selectedNode.type)) { // check for same type as selected node to not overwrite it
addToWorkspace(node);
}
});
});
}
} else {
console.warn(`Node type "${selectedNode.type}" is not allowed in the workspace.`);
}
};

return (
<div className="info-box">
<h3>Concept Details</h3>
<h3>Node Details</h3>
{selectedNode ? (
<div>
<div className="node-info-container">
Expand All @@ -48,7 +72,7 @@ const InfoBoxComponent: React.FC<IInfoBoxProps> = ({
</div>
<button
className="add-button"
onClick={() => addToWorkspace(selectedNode)}
onClick={handleAddToWorkspace}
disabled={!isAddable}
>
Add to Workspace
Expand Down
25 changes: 23 additions & 2 deletions src/components/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface INodeRelation {
const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {
const [parents, setParents] = useState<INodeRelation[]>([]);
const [children, setChildren] = useState<INodeRelation[]>([]);
const [requirements, setRequirements] = useState<string[]>([]);

useEffect(() => {
if (!selectedNode) {
Expand Down Expand Up @@ -45,6 +46,12 @@ const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {

fetchAndSetParents();
fetchAndSetChildren();

if (selectedNode.requires && selectedNode.requires.length > 0) {
setRequirements(selectedNode.requires);
} else {
setRequirements([]);
}
}, [selectedNode]);

const processNodeRelations = (
Expand All @@ -68,10 +75,12 @@ const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {

return (
<div className="tree-view">
<h3>Node Connections</h3>
{selectedNode ? (
<>
<h2 style={{ textAlign: 'center' }}>{selectedNode.label}</h2>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{/* Parents Column */}
<div>
<h3>Parents</h3>
{parents.length > 0 ? (
Expand All @@ -87,7 +96,8 @@ const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {
)}
</div>

<div>
{/* Children Column */}
<div style={{ marginLeft: '20px' }}>
<h3>Children</h3>
{children.length > 0 ? (
<ul>
Expand All @@ -101,10 +111,21 @@ const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {
<p>No children found.</p>
)}
</div>
{/* Requires Column */}
{requirements.length > 0 && (
<div style={{ marginLeft: '20px' }}>
<h3>Requires</h3>
<ul>
{requirements.map((requirement, index) => (
<li key={index}>{requirement}</li>
))}
</ul>
</div>
)}
</div>
</>
) : (
<p>Please select a node first</p>
<p>Please select a node to see its connections</p>
)}
</div>
);
Expand Down
136 changes: 110 additions & 26 deletions src/components/Workspace.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { IWorkspaceProps } from './interfaces/WorkspaceInterfaces';
import { exportWorkspace } from '../handler';
import { exportWorkspace, runSimulation } from '../handler';

const WorkspaceComponent: React.FC<IWorkspaceProps> = ({ workspace }) => {
const [exportType, setExportType] = useState('python file');
const WorkspaceComponent: React.FC<IWorkspaceProps> = ({ workspace, updateConnectivityOptions, resetWorkspace }) => {
const [exportType, setExportType] = useState('py');
const [message, setMessage] = useState<string | null>(null);
const [filename, setFilename] = useState('workspace_export'); // Default filename
const [directory, setDirectory] = useState('');

const handleExport = async () => {
const parcellationOptions = ['DesikanKilliany', 'Destrieux', 'Schaefer1000', 'hcpmmp1', 'virtualdbs'];
const tractogramOptions = ['MghUscHcp32', 'PPMI85', 'dTOR'];

const constructNodeData = () => {
const nodeData = {
model: workspace.model ? workspace.model.label : 'None',
connectivity: workspace.connectivity ? workspace.connectivity.label : 'None',
parcellation: workspace.parcellation ? workspace.parcellation : 'None',
tractogram: workspace.tractogram ? workspace.tractogram : 'None',
coupling: workspace.coupling ? workspace.coupling.label : 'None',
noise: workspace.noise ? workspace.noise.label : 'None',
integrationMethod: workspace.integrationMethod ? workspace.integrationMethod.label : 'None',
};

return nodeData;
};

const handleExport = async () => {
const nodeData = constructNodeData();

try {
const response = await exportWorkspace(exportType, nodeData, filename);
const response = await exportWorkspace(exportType, nodeData, directory);

if (response.status === 'success') {
setMessage(`File saved successfully: ${response.message}`);
setMessage(`${response.message}`);
} else {
setMessage(`Failed to save file: ${response.error}`);
}
Expand All @@ -33,16 +43,83 @@ const WorkspaceComponent: React.FC<IWorkspaceProps> = ({ workspace }) => {
}
};

const handleRunSimulation = async () => {
const nodeData = constructNodeData();

try {
const response = await runSimulation(exportType, nodeData, directory);

if (response.status === 'success') {
setMessage(`${response.message}`);
} else {
setMessage(`${response.error}`);
}
} catch (error) {
if (error instanceof Error) {
setMessage(`Error during simulation run: ${error.message}`);
} else {
setMessage('An unknown error occurred during simulation run.');
}
}
};

// set timer for success/error message
useEffect(() => {
if (message) {
const timer = setTimeout(() => {
setMessage(null);
}, 30000);

return () => clearTimeout(timer);
}
}, [message]);

return (
<div className="workspace">
<h3>Workspace</h3>
<div className="workspace-header">
<h3>Workspace</h3>
<button className="reset-button" onClick={resetWorkspace}>Reset Workspace</button>
</div>
<div>
<h4>Model</h4>
<p>{workspace.model ? workspace.model.label : 'None'}</p>
</div>
<div>
<h4>Connectivity</h4>
<p>{workspace.connectivity ? workspace.connectivity.label : 'None'}</p>
<div className="dropdown-container">
{/* Parcellation Dropdown */}
<div className="dropdown-section">
<label htmlFor="parcellation">Parcellation:</label>
<select
id="parcellation"
value={workspace.parcellation || ''}
onChange={(e) => updateConnectivityOptions('parcellation', e.target.value)} // Update state on change
>
<option value="" disabled>Select a parcellation</option>
{parcellationOptions.map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
</div>

{/* Tractogram Dropdown */}
<div className="dropdown-section">
<label htmlFor="tractogram">Tractogram:</label>
<select
id="tractogram"
value={workspace.tractogram || ''}
onChange={(e) => updateConnectivityOptions('tractogram', e.target.value)} // Update state on change
>
<option value="" disabled>Select a tractogram</option>
{tractogramOptions.map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
</div>
</div>
</div>
<div>
<h4>Coupling</h4>
Expand All @@ -60,28 +137,35 @@ const WorkspaceComponent: React.FC<IWorkspaceProps> = ({ workspace }) => {
{/* Export controls */}
<div className="export-controls">
<div className="export-control">
<label htmlFor="export-type">Select export type: </label>
<select
id="export-type"
value={exportType}
onChange={(e) => setExportType(e.target.value)}
>
<option value="python file">Python File</option>
<option value="xml file">XML File</option>
</select>
<div className="dropdown-section">
<label htmlFor="export-type">Select export type: </label>
<select
id="export-type"
value={exportType}
onChange={(e) => setExportType(e.target.value)}
>
<option value="py">Simulation code (.py)</option>
<option value="xml">Model specification (.xml)</option>
<option value="yaml">Metadata (.yaml)</option>
</select>
</div>
</div>

<div className="export-control">
<label htmlFor="filename">Filename: </label>
<label htmlFor="directory">Save Directory Path: </label>
<input
id="filename"
id="directory"
type="text"
value={filename}
onChange={(e) => setFilename(e.target.value)}
value={directory}
onChange={(e) => setDirectory(e.target.value)}
placeholder="/home/user/downloads"
/>
</div>

<button className="export-button" onClick={handleExport}>Export</button>
<div className="button-container">
<button className="export-button" onClick={handleExport}>Export</button>
<button className="export-button" onClick={handleRunSimulation}>Run Simulation</button>
</div>
{message && <p>{message}</p>} {/* Display success/error message */}
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/interfaces/GraphViewInterfaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface INodeType {
type: string;
definition: string;
iri: string;
requires: string[];
x?: number;
y?: number;
collapsed?: boolean;
Expand Down
Loading

0 comments on commit 4196844

Please sign in to comment.