diff --git a/applications/visualizer/frontend/src/components/CreateNewWorkspaceDialog.tsx b/applications/visualizer/frontend/src/components/CreateNewWorkspaceDialog.tsx index 96e1f253..89c8155d 100644 --- a/applications/visualizer/frontend/src/components/CreateNewWorkspaceDialog.tsx +++ b/applications/visualizer/frontend/src/components/CreateNewWorkspaceDialog.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import { useGlobalContext } from "../contexts/GlobalContext.tsx"; import { CaretIcon, CheckIcon, CloseIcon } from "../icons"; -import {type Dataset, type Neuron, NeuronsService} from "../rest"; +import { type Dataset, type Neuron, NeuronsService } from "../rest"; import { vars as colors } from "../theme/variables.ts"; import CustomAutocomplete from "./CustomAutocomplete.tsx"; import CustomDialog from "./CustomDialog.tsx"; diff --git a/applications/visualizer/frontend/src/components/ViewerContainer/CustomEntitiesDropdown.tsx b/applications/visualizer/frontend/src/components/ViewerContainer/CustomEntitiesDropdown.tsx index e694f273..051327e8 100644 --- a/applications/visualizer/frontend/src/components/ViewerContainer/CustomEntitiesDropdown.tsx +++ b/applications/visualizer/frontend/src/components/ViewerContainer/CustomEntitiesDropdown.tsx @@ -5,7 +5,7 @@ import type React from "react"; import { useEffect, useRef, useState } from "react"; import { CheckIcon } from "../../icons"; import { vars } from "../../theme/variables.ts"; -import type {EnhancedNeuron} from "../../models/models.ts"; +import type { EnhancedNeuron } from "../../models/models.ts"; const { gray50, brand600 } = vars; diff --git a/applications/visualizer/frontend/src/components/ViewerContainer/CustomSwitch.tsx b/applications/visualizer/frontend/src/components/ViewerContainer/CustomSwitch.tsx index 2b0fcca0..8fc8fda7 100644 --- a/applications/visualizer/frontend/src/components/ViewerContainer/CustomSwitch.tsx +++ b/applications/visualizer/frontend/src/components/ViewerContainer/CustomSwitch.tsx @@ -17,7 +17,6 @@ interface CustomSwitchProps { } const CustomSwitch: React.FC = ({ width, height, thumbDimension, checkedPosition, checked, onChange, showTooltip, disabled }) => { - return ( ({ - id: neuron, - label: neuron, - checked: isActive, + id: neuron, + label: neuron, + checked: isActive, }); const mapNeuronsAvailableNeuronsToOptions = (neuron: Neuron) => ({ - id: neuron.name, - label: neuron.name, - content: [], + id: neuron.name, + label: neuron.name, + content: [], }); -const Neurons = ({children}) => { - const {workspaces, datasets, currentWorkspaceId} = useGlobalContext(); - const currentWorkspace = workspaces[currentWorkspaceId]; - const activeNeurons = currentWorkspace.activeNeurons; - const recentNeurons = Object.values(currentWorkspace.availableNeurons).filter( - (neuron) => neuron.isInteractant - ); - const availableNeurons = currentWorkspace.availableNeurons; +const Neurons = ({ children }) => { + const { workspaces, datasets, currentWorkspaceId } = useGlobalContext(); + const currentWorkspace = workspaces[currentWorkspaceId]; + const activeNeurons = currentWorkspace.activeNeurons; + const recentNeurons = Object.values(currentWorkspace.availableNeurons).filter((neuron) => neuron.isInteractant); + const availableNeurons = currentWorkspace.availableNeurons; - const [neurons, setNeurons] = useState(availableNeurons); + const [neurons, setNeurons] = useState(availableNeurons); - const handleSwitchChange = async (neuronId: string, checked: boolean) => { - const neuron = availableNeurons[neuronId]; + const handleSwitchChange = async (neuronId: string, checked: boolean) => { + const neuron = availableNeurons[neuronId]; - if (!neuron) return; - if (checked) { - await currentWorkspace.activateNeuron(neuron); - } else { - await currentWorkspace.deactivateNeuron(neuronId); - } - }; + if (!neuron) return; + if (checked) { + await currentWorkspace.activateNeuron(neuron); + } else { + await currentWorkspace.deactivateNeuron(neuronId); + } + }; - const onNeuronClick = (option) => { - const neuron = availableNeurons[option.id]; - if (neuron && !activeNeurons.has(option.id)) { - currentWorkspace.activateNeuron(neuron); - } else { - currentWorkspace.deleteNeuron(option.id); - } - }; - const handleDeleteNeuron = (neuronId: string) => { - currentWorkspace.deleteNeuron(neuronId); - }; + const onNeuronClick = (option) => { + const neuron = availableNeurons[option.id]; + if (neuron && !activeNeurons.has(option.id)) { + currentWorkspace.activateNeuron(neuron); + } else { + currentWorkspace.deleteNeuron(option.id); + } + }; + const handleDeleteNeuron = (neuronId: string) => { + currentWorkspace.deleteNeuron(neuronId); + }; - const fetchNeurons = async (name: string, datasetsIds: { id: string }[]) => { - try { - const ids = datasetsIds.map((dataset) => dataset.id); - const response = await NeuronsService.searchCells({name: name, datasetIds: ids}); + const fetchNeurons = async (name: string, datasetsIds: { id: string }[]) => { + try { + const ids = datasetsIds.map((dataset) => dataset.id); + const response = await NeuronsService.searchCells({ name: name, datasetIds: ids }); - // Convert the object to a Record - const neuronsRecord = Object.entries(response).reduce((acc: Record, [_, neuron]: [string, EnhancedNeuron]) => { - acc[neuron.name] = neuron; - return acc; - }, {}); + // Convert the object to a Record + const neuronsRecord = Object.entries(response).reduce((acc: Record, [_, neuron]: [string, EnhancedNeuron]) => { + acc[neuron.name] = neuron; + return acc; + }, {}); - setNeurons(neuronsRecord); - } catch (error) { - console.error("Failed to fetch datasets", error); - } - }; + setNeurons(neuronsRecord); + } catch (error) { + console.error("Failed to fetch datasets", error); + } + }; - const debouncedFetchNeurons = useCallback(debounce(fetchNeurons, 300), []); + const debouncedFetchNeurons = useCallback(debounce(fetchNeurons, 300), []); - const onSearchNeurons = (value) => { - const datasetsIds = Object.keys(datasets); - debouncedFetchNeurons(value, datasetsIds); - }; + const onSearchNeurons = (value) => { + const datasetsIds = Object.keys(datasets); + debouncedFetchNeurons(value, datasetsIds); + }; - const autoCompleteOptions = Object.values(neurons).map((neuron: Neuron) => mapNeuronsAvailableNeuronsToOptions(neuron)); + const autoCompleteOptions = Object.values(neurons).map((neuron: Neuron) => mapNeuronsAvailableNeuronsToOptions(neuron)); - return ( - - - - Neurons - + return ( + + + + Neurons + - - Search for the neurons and add it to your workspace. This will affect all viewers. - - - {children} - - + Search for the neurons and add it to your workspace. This will affect all viewers. + + + {children} + + + + + + All Neurons + + + - - - - All Neurons - - - - - - - - {Array.from(recentNeurons).map((neuron) => ( - - ))} - - - - ); + > + + + + + {Array.from(recentNeurons).map((neuron) => ( + + ))} + + + + ); }; export default Neurons; diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx index a1260fc1..87766e74 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx @@ -24,7 +24,7 @@ import Gizmo from "./Gizmo.tsx"; import Loader from "./Loader.tsx"; import STLViewer from "./STLViewer.tsx"; import SceneControls from "./SceneControls.tsx"; -import type {Dataset} from "../../../rest"; +import type { Dataset } from "../../../rest"; const { gray100, gray600 } = vars; export interface Instance { diff --git a/applications/visualizer/frontend/src/models/models.ts b/applications/visualizer/frontend/src/models/models.ts index ad742a6e..cc06870b 100644 --- a/applications/visualizer/frontend/src/models/models.ts +++ b/applications/visualizer/frontend/src/models/models.ts @@ -28,7 +28,7 @@ export interface NeuronGroup { export interface EnhancedNeuron extends Neuron { viewerData: ViewerData; - isInteractant: boolean + isInteractant: boolean; } export interface GraphViewerData { diff --git a/applications/visualizer/frontend/src/models/workspace.ts b/applications/visualizer/frontend/src/models/workspace.ts index d5e89d34..af721cbc 100644 --- a/applications/visualizer/frontend/src/models/workspace.ts +++ b/applications/visualizer/frontend/src/models/workspace.ts @@ -1,225 +1,219 @@ -import {produce, immerable} from "immer"; -import type {configureStore} from "@reduxjs/toolkit"; -import {type EnhancedNeuron, type NeuronGroup, ViewerSynchronizationPair, ViewerType} from "./models"; +import { produce, immerable } from "immer"; +import type { configureStore } from "@reduxjs/toolkit"; +import { type EnhancedNeuron, type NeuronGroup, ViewerSynchronizationPair, ViewerType } from "./models"; import getLayoutManagerAndStore from "../layout-manager/layoutManagerFactory"; -import {type Dataset, type Neuron, NeuronsService} from "../rest"; -import type {LayoutManager} from "@metacell/geppetto-meta-client/common/layout/LayoutManager"; +import { type Dataset, type Neuron, NeuronsService } from "../rest"; +import type { LayoutManager } from "@metacell/geppetto-meta-client/common/layout/LayoutManager"; export class Workspace { - [immerable] = true; - - id: string; - name: string; - // datasetID -> Dataset - activeDatasets: Record; - // neuronID -> Neurons - availableNeurons: Record; - // neuronId - activeNeurons: Set; - selectedNeurons: Set; - viewers: Record; - synchronizations: Record; - neuronGroups: Record; - - store: ReturnType; - layoutManager: LayoutManager; - updateContext: (workspace: Workspace) => void; - - constructor(id: string, - name: string, - activeDatasets: Record, - activeNeurons: Set, - updateContext: (workspace: Workspace) => void) { - this.id = id; - this.name = name; - this.activeDatasets = activeDatasets; - this.availableNeurons = {}; - this.activeNeurons = activeNeurons || new Set(); - this.selectedNeurons = new Set(); - this.viewers = { - [ViewerType.Graph]: false, - [ViewerType.ThreeD]: true, - [ViewerType.EM]: false, - [ViewerType.InstanceDetails]: false, - }; - this.synchronizations = { - [ViewerSynchronizationPair.Graph_InstanceDetails]: true, - [ViewerSynchronizationPair.Graph_ThreeD]: true, - [ViewerSynchronizationPair.ThreeD_EM]: true, - }; - this.neuronGroups = {}; - - const {layoutManager, store} = getLayoutManagerAndStore(id); - this.layoutManager = layoutManager; - this.store = store; - this.updateContext = updateContext; - - this._initializeAvailableNeurons(); - } - - activateNeuron(neuron: Neuron): void { - const updated = produce(this, (draft: Workspace) => { - draft.activeNeurons.add(neuron.name); - // Set isInteractant to true if the neuron exists in availableNeurons - if (draft.availableNeurons[neuron.name]) { - draft.availableNeurons[neuron.name].isInteractant = true; - } - }); - - this.updateContext(updated); - } - - deactivateNeuron(neuronId: string): void { - const updated = produce(this, (draft: Workspace) => { - draft.activeNeurons.delete(neuronId); - }); - this.updateContext(updated); - } - - deleteNeuron(neuronId: string): void { - const updated = produce(this, (draft: Workspace) => { - // Remove the neuron from activeNeurons - draft.activeNeurons.delete(neuronId); - - // Set isInteractant to false if the neuron exists in availableNeurons - if (draft.availableNeurons[neuronId]) { - draft.availableNeurons[neuronId].isInteractant = false; - } - }); - this.updateContext(updated); - } - - async activateDataset(dataset: Dataset): Promise { - const updated: Workspace = produce(this, (draft: Workspace) => { - draft.activeDatasets[dataset.id] = dataset; - }); - const updatedWithNeurons = await this._getAvailableNeurons(updated); - this.updateContext(updatedWithNeurons); - } - - async deactivateDataset(datasetId: string): Promise { - const updated: Workspace = produce(this, (draft: Workspace) => { - delete draft.activeDatasets[datasetId]; - }); - - const updatedWithNeurons = await this._getAvailableNeurons(updated); - this.updateContext(updatedWithNeurons); - } - - toggleSelectedNeuron(neuronId: string): void { - const updated = produce(this, (draft: Workspace) => { - if (draft.selectedNeurons.has(neuronId)) { - draft.selectedNeurons.delete(neuronId); - } else { - draft.selectedNeurons.add(neuronId); - } - }); - this.updateContext(updated); - } - - setActiveNeurons(newActiveNeurons: Set): void { - const updated = produce(this, (draft: Workspace) => { - draft.activeNeurons = newActiveNeurons; - }); - this.updateContext(updated); - } - - clearSelectedNeurons(): void { - const updated = produce(this, (draft: Workspace) => { - draft.selectedNeurons.clear(); - }); - this.updateContext(updated); - } - - updateViewerSynchronizationStatus(pair: ViewerSynchronizationPair, isActive: boolean): void { - const updated = produce(this, (draft: Workspace) => { - draft.synchronizations[pair] = isActive; - }); - this.updateContext(updated); - } - - addNeuronToGroup(neuronId: string, groupId: string): void { - const updated = produce(this, (draft: Workspace) => { - if (!draft.activeNeurons[neuronId]) { - throw new Error("Neuron not found"); - } - const group = draft.neuronGroups[groupId]; - if (!group) { - throw new Error("Neuron group not found"); - } - group.neurons.add(neuronId); - }); - this.updateContext(updated); - } - - createNeuronGroup(neuronGroup: NeuronGroup): void { - const updated = produce(this, (draft: Workspace) => { - draft.neuronGroups[neuronGroup.id] = neuronGroup; - }); - this.updateContext(updated); - } - - changeViewerVisibility(viewerId: ViewerType, isVisible: boolean): void { - const updated = produce(this, (draft: Workspace) => { - if (draft.viewers[viewerId] === undefined) { - throw new Error("Viewer not found"); - } - draft.viewers[viewerId] = isVisible; - }); - this.updateContext(updated); - } - - async _initializeAvailableNeurons() { - const updatedWithNeurons = await this._getAvailableNeurons(this); - this.updateContext(updatedWithNeurons); - } - - - async _getAvailableNeurons(updatedWorkspace: Workspace): Promise { - try { - const datasetIds = Object.keys(updatedWorkspace.activeDatasets); - const neuronArrays = await NeuronsService.searchCells({datasetIds}); - - const uniqueNeurons = new Set(); - - // Flatten and deduplicate neurons - for (const neuronArray of neuronArrays.flat()) { - uniqueNeurons.add(neuronArray); - const classNeuron = {...neuronArray, name: neuronArray.nclass}; - uniqueNeurons.add(classNeuron); - } - - return produce(updatedWorkspace, (draft: Workspace) => { - draft.availableNeurons = {}; - for (const neuron of uniqueNeurons) { - const previousNeuron = draft.availableNeurons[neuron.name]; - - const enhancedNeuron: EnhancedNeuron = { - ...neuron, - viewerData: { - [ViewerType.Graph]: { - defaultPosition: previousNeuron?.viewerData[ViewerType.Graph]?.defaultPosition || null, - visibility: previousNeuron?.viewerData[ViewerType.Graph]?.visibility || false, - }, - [ViewerType.ThreeD]: previousNeuron?.viewerData[ViewerType.ThreeD] || {}, - [ViewerType.EM]: previousNeuron?.viewerData[ViewerType.EM] || {}, - [ViewerType.InstanceDetails]: previousNeuron?.viewerData[ViewerType.InstanceDetails] || {}, - }, - isInteractant: previousNeuron?.isInteractant ?? draft.activeNeurons.has(neuron.name) - }; - - draft.availableNeurons[neuron.name] = enhancedNeuron; - } - }); - } catch (error) { - console.error("Failed to fetch neurons:", error); - return updatedWorkspace; + [immerable] = true; + + id: string; + name: string; + // datasetID -> Dataset + activeDatasets: Record; + // neuronID -> Neurons + availableNeurons: Record; + // neuronId + activeNeurons: Set; + selectedNeurons: Set; + viewers: Record; + synchronizations: Record; + neuronGroups: Record; + + store: ReturnType; + layoutManager: LayoutManager; + updateContext: (workspace: Workspace) => void; + + constructor(id: string, name: string, activeDatasets: Record, activeNeurons: Set, updateContext: (workspace: Workspace) => void) { + this.id = id; + this.name = name; + this.activeDatasets = activeDatasets; + this.availableNeurons = {}; + this.activeNeurons = activeNeurons || new Set(); + this.selectedNeurons = new Set(); + this.viewers = { + [ViewerType.Graph]: false, + [ViewerType.ThreeD]: true, + [ViewerType.EM]: false, + [ViewerType.InstanceDetails]: false, + }; + this.synchronizations = { + [ViewerSynchronizationPair.Graph_InstanceDetails]: true, + [ViewerSynchronizationPair.Graph_ThreeD]: true, + [ViewerSynchronizationPair.ThreeD_EM]: true, + }; + this.neuronGroups = {}; + + const { layoutManager, store } = getLayoutManagerAndStore(id); + this.layoutManager = layoutManager; + this.store = store; + this.updateContext = updateContext; + + this._initializeAvailableNeurons(); + } + + activateNeuron(neuron: Neuron): void { + const updated = produce(this, (draft: Workspace) => { + draft.activeNeurons.add(neuron.name); + // Set isInteractant to true if the neuron exists in availableNeurons + if (draft.availableNeurons[neuron.name]) { + draft.availableNeurons[neuron.name].isInteractant = true; + } + }); + + this.updateContext(updated); + } + + deactivateNeuron(neuronId: string): void { + const updated = produce(this, (draft: Workspace) => { + draft.activeNeurons.delete(neuronId); + }); + this.updateContext(updated); + } + + deleteNeuron(neuronId: string): void { + const updated = produce(this, (draft: Workspace) => { + // Remove the neuron from activeNeurons + draft.activeNeurons.delete(neuronId); + + // Set isInteractant to false if the neuron exists in availableNeurons + if (draft.availableNeurons[neuronId]) { + draft.availableNeurons[neuronId].isInteractant = false; + } + }); + this.updateContext(updated); + } + + async activateDataset(dataset: Dataset): Promise { + const updated: Workspace = produce(this, (draft: Workspace) => { + draft.activeDatasets[dataset.id] = dataset; + }); + const updatedWithNeurons = await this._getAvailableNeurons(updated); + this.updateContext(updatedWithNeurons); + } + + async deactivateDataset(datasetId: string): Promise { + const updated: Workspace = produce(this, (draft: Workspace) => { + delete draft.activeDatasets[datasetId]; + }); + + const updatedWithNeurons = await this._getAvailableNeurons(updated); + this.updateContext(updatedWithNeurons); + } + + toggleSelectedNeuron(neuronId: string): void { + const updated = produce(this, (draft: Workspace) => { + if (draft.selectedNeurons.has(neuronId)) { + draft.selectedNeurons.delete(neuronId); + } else { + draft.selectedNeurons.add(neuronId); + } + }); + this.updateContext(updated); + } + + setActiveNeurons(newActiveNeurons: Set): void { + const updated = produce(this, (draft: Workspace) => { + draft.activeNeurons = newActiveNeurons; + }); + this.updateContext(updated); + } + + clearSelectedNeurons(): void { + const updated = produce(this, (draft: Workspace) => { + draft.selectedNeurons.clear(); + }); + this.updateContext(updated); + } + + updateViewerSynchronizationStatus(pair: ViewerSynchronizationPair, isActive: boolean): void { + const updated = produce(this, (draft: Workspace) => { + draft.synchronizations[pair] = isActive; + }); + this.updateContext(updated); + } + + addNeuronToGroup(neuronId: string, groupId: string): void { + const updated = produce(this, (draft: Workspace) => { + if (!draft.activeNeurons[neuronId]) { + throw new Error("Neuron not found"); + } + const group = draft.neuronGroups[groupId]; + if (!group) { + throw new Error("Neuron group not found"); + } + group.neurons.add(neuronId); + }); + this.updateContext(updated); + } + + createNeuronGroup(neuronGroup: NeuronGroup): void { + const updated = produce(this, (draft: Workspace) => { + draft.neuronGroups[neuronGroup.id] = neuronGroup; + }); + this.updateContext(updated); + } + + changeViewerVisibility(viewerId: ViewerType, isVisible: boolean): void { + const updated = produce(this, (draft: Workspace) => { + if (draft.viewers[viewerId] === undefined) { + throw new Error("Viewer not found"); + } + draft.viewers[viewerId] = isVisible; + }); + this.updateContext(updated); + } + + async _initializeAvailableNeurons() { + const updatedWithNeurons = await this._getAvailableNeurons(this); + this.updateContext(updatedWithNeurons); + } + + async _getAvailableNeurons(updatedWorkspace: Workspace): Promise { + try { + const datasetIds = Object.keys(updatedWorkspace.activeDatasets); + const neuronArrays = await NeuronsService.searchCells({ datasetIds }); + + const uniqueNeurons = new Set(); + + // Flatten and deduplicate neurons + for (const neuronArray of neuronArrays.flat()) { + uniqueNeurons.add(neuronArray); + const classNeuron = { ...neuronArray, name: neuronArray.nclass }; + uniqueNeurons.add(classNeuron); + } + + return produce(updatedWorkspace, (draft: Workspace) => { + draft.availableNeurons = {}; + for (const neuron of uniqueNeurons) { + const previousNeuron = draft.availableNeurons[neuron.name]; + + const enhancedNeuron: EnhancedNeuron = { + ...neuron, + viewerData: { + [ViewerType.Graph]: { + defaultPosition: previousNeuron?.viewerData[ViewerType.Graph]?.defaultPosition || null, + visibility: previousNeuron?.viewerData[ViewerType.Graph]?.visibility || false, + }, + [ViewerType.ThreeD]: previousNeuron?.viewerData[ViewerType.ThreeD] || {}, + [ViewerType.EM]: previousNeuron?.viewerData[ViewerType.EM] || {}, + [ViewerType.InstanceDetails]: previousNeuron?.viewerData[ViewerType.InstanceDetails] || {}, + }, + isInteractant: previousNeuron?.isInteractant ?? draft.activeNeurons.has(neuron.name), + }; + + draft.availableNeurons[neuron.name] = enhancedNeuron; } + }); + } catch (error) { + console.error("Failed to fetch neurons:", error); + return updatedWorkspace; } + } - - customUpdate(updateFunction: (draft: Workspace) => void): void { - const updated = produce(this, updateFunction); - this.updateContext(updated); - } + customUpdate(updateFunction: (draft: Workspace) => void): void { + const updated = produce(this, updateFunction); + this.updateContext(updated); + } }