Skip to content

Commit e7e05d6

Browse files
Refactor frontend
1 parent c9d8f67 commit e7e05d6

File tree

9 files changed

+198
-301
lines changed

9 files changed

+198
-301
lines changed

frontend/src/components/Simulation/InputStep.tsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,16 @@ import noModelImage from "@/assets/no-model-light.svg";
55
import CenteredImage from "@/components/CenteredImage";
66

77
type InputStepProps = {
8-
currentPrimaryModel?: ModelConfig;
9-
currentSecondaryModel?: ModelConfig;
8+
selectedModels: ModelConfig[];
109
setModelInput: (modelInput: any) => void;
1110
};
1211

13-
const InputStep: React.FC<InputStepProps> = ({ currentPrimaryModel, currentSecondaryModel, setModelInput }) => {
14-
const selectedModel = currentPrimaryModel || currentSecondaryModel;
15-
16-
const isChainSimulation = currentPrimaryModel !== undefined && currentSecondaryModel !== undefined;
17-
18-
if (selectedModel !== undefined) {
19-
if (isChainSimulation) {
20-
return (
21-
<ModelInputs model={selectedModel} secondaryModel={currentSecondaryModel} onSubmit={setModelInput} />
22-
);
23-
} else {
24-
return <ModelInputs model={selectedModel} onSubmit={setModelInput} />;
25-
}
26-
} else {
12+
const InputStep: React.FC<InputStepProps> = ({ selectedModels, setModelInput }) => {
13+
if (selectedModels.length === 0) {
2714
return <CenteredImage src={noModelImage} caption="No model selected" />;
2815
}
16+
17+
return <ModelInputs selectedModels={selectedModels} onSubmit={setModelInput} />;
2918
};
3019

3120
export default InputStep;

frontend/src/components/Simulation/ModelInputs.tsx

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import ConvertibleTextField from "../ConvertibleTextField";
55
import { FORMULA_TO_NAME_MAPPER } from "@/constants/formula_map";
66
import { MetaTooltip } from "@/functions/Tooltip";
77
import { Columns } from "@/components/styles";
8-
import { useModelInputStore } from "@/hooks/useModelInputStore";
8+
import { useModelInputStore, getModelInputStore } from "@/hooks/useModelInputStore";
99
import { useShallow } from "zustand/react/shallow";
1010
import { ModelInput } from "@/dto/ModelInput";
11+
import { useConcentrationsStore } from "@/hooks/useConcentrationsStore";
12+
import { sortModelsByCategory } from "@/utils/modelUtils";
1113

1214
const PPM_MAX = 1000000;
1315

@@ -59,7 +61,7 @@ function ParametersInput({
5961

6062
return (
6163
<div style={{ display: "flex", flexDirection: "column", gap: "16px", flexGrow: 1 }}>
62-
<Typography variant="h3"> {model.category} Parameters </Typography>
64+
<Typography variant="h3">{model.displayName} Parameters</Typography>
6365
{Object.entries(model.parameters).map(([name, config]) =>
6466
config.choices ? (
6567
<NativeSelect
@@ -92,34 +94,44 @@ function ParametersInput({
9294
);
9395
}
9496

95-
const ModelInputs: React.FC<{
96-
model: ModelConfig;
97-
secondaryModel?: ModelConfig;
98-
onSubmit: (modelInput: ModelInput) => void;
99-
}> = ({ model, secondaryModel, onSubmit }) => {
100-
const { concentrations, parameters, setConcentration, setParameter } = useModelInputStore(
97+
function ModelParametersWrapper({ model }: { model: ModelConfig }) {
98+
const { parameters, setParameter } = useModelInputStore(
10199
model,
102100
useShallow((s) => ({
103-
concentrations: s.concentrations,
104101
parameters: s.parameters,
105-
setConcentration: s.setConcentration,
106102
setParameter: s.setParameter,
107103
}))
108104
);
109105

110-
const secondaryModelStore = useModelInputStore(
111-
secondaryModel ?? model,
106+
return <ParametersInput model={model} parameters={parameters} setParameter={setParameter} />;
107+
}
108+
109+
const ModelInputs: React.FC<{
110+
selectedModels: ModelConfig[];
111+
onSubmit: (modelInput: ModelInput) => void;
112+
}> = ({ selectedModels, onSubmit }) => {
113+
const { concentrations, setConcentration } = useConcentrationsStore(
112114
useShallow((s) => ({
113-
parameters: s.parameters,
114-
setParameter: s.setParameter,
115+
concentrations: s.concentrations,
116+
setConcentration: s.setConcentration,
115117
}))
116118
);
117119

118-
const { parameters: secondaryParameters, setParameter: setSecondaryParameter } = secondaryModel
119-
? secondaryModelStore
120-
: { parameters: undefined, setParameter: undefined };
120+
const allValidSubstances = new Set(selectedModels.flatMap((m) => m.validSubstances));
121+
const invisible = Array.from(allValidSubstances).filter((name) => concentrations[name] === undefined);
121122

122-
const invisible = model.validSubstances.filter((name) => concentrations[name] === undefined);
123+
const handleSubmit = () => {
124+
const models = sortModelsByCategory(selectedModels).map((model) => {
125+
const store = getModelInputStore(model);
126+
const parameters = store.getState().parameters;
127+
return {
128+
modelId: model.modelId,
129+
parameters: parameters,
130+
};
131+
});
132+
133+
onSubmit({ concentrations, models });
134+
};
123135

124136
return (
125137
<div>
@@ -143,19 +155,11 @@ const ModelInputs: React.FC<{
143155
))}
144156
<SubstanceAdder invisible={invisible} onAdd={(item: string) => setConcentration(item, 0)} />
145157
</div>
146-
<ParametersInput model={model} parameters={parameters} setParameter={setParameter} />
147-
{secondaryModel && secondaryParameters && setSecondaryParameter && (
148-
<ParametersInput
149-
model={secondaryModel}
150-
parameters={secondaryParameters}
151-
setParameter={setSecondaryParameter}
152-
/>
153-
)}
158+
{selectedModels.map((model) => (
159+
<ModelParametersWrapper key={model.modelId} model={model} />
160+
))}
154161
</Columns>
155-
<Button
156-
style={{ marginTop: "1em" }}
157-
onClick={() => onSubmit({ models: [{ modelId: model.modelId, parameters }], concentrations })}
158-
>
162+
<Button style={{ marginTop: "1em" }} onClick={handleSubmit}>
159163
Run Simulation
160164
</Button>
161165
</div>

frontend/src/components/Simulation/ModelSelect.tsx

Lines changed: 46 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,69 +3,24 @@ import { Button, Dialog, Icon, Typography } from "@equinor/eds-core-react";
33
import { ModelConfig } from "@/dto/FormConfig";
44
import { useAvailableModels } from "@/contexts/ModelContext";
55
import { help } from "@equinor/eds-icons";
6+
import { sortModelsByCategory } from "@/utils/modelUtils";
67

7-
const makeLabel = (modelConfig: ModelConfig) =>
8-
modelConfig.accessError ? `{modelConfig.displayName} (Access Error)` : modelConfig.displayName;
9-
10-
const handleToggle = (
11-
isActive: boolean,
12-
setModel: (modelConfig: ModelConfig | undefined) => void,
13-
modelConfig: ModelConfig
14-
) => {
15-
if (isActive) {
16-
// If currently active, unselect it
17-
setModel(undefined);
18-
} else {
19-
// If not active, select it
20-
setModel(modelConfig);
21-
}
22-
};
23-
24-
const PrimaryModelButton: React.FC<{
25-
modelConfig: ModelConfig;
26-
active: boolean;
27-
setCurrentPrimaryModel: (modelConfig: ModelConfig | undefined) => void;
28-
}> = ({ modelConfig, active, setCurrentPrimaryModel }) => {
29-
const [open, setOpen] = useState<boolean>(false);
30-
31-
return (
32-
<Button.Group>
33-
<Button
34-
variant={active ? "outlined" : "contained"}
35-
onClick={() => {
36-
handleToggle(active, setCurrentPrimaryModel, modelConfig);
37-
}}
38-
disabled={!!modelConfig.accessError}
39-
>
40-
{makeLabel(modelConfig)}
41-
</Button>
42-
<Button variant={"contained"} onClick={() => setOpen(true)}>
43-
<Icon data={help} />
44-
</Button>
45-
<Dialog open={open} onClose={() => setOpen(false)} isDismissable={true} style={{ width: "100%" }}>
46-
<Dialog.Header>
47-
<Dialog.Title>Model {modelConfig.displayName}</Dialog.Title>
48-
</Dialog.Header>
49-
<Dialog.Content style={{ whiteSpace: "pre-wrap" }}>{modelConfig.description}</Dialog.Content>
50-
</Dialog>
51-
</Button.Group>
52-
);
8+
const makeLabel = (modelConfig: ModelConfig) => {
9+
return modelConfig.accessError ? `${modelConfig.displayName} (Access Error)` : modelConfig.displayName;
5310
};
5411

55-
const SecondaryModelButton: React.FC<{
12+
const ModelButton: React.FC<{
5613
modelConfig: ModelConfig;
57-
active: boolean;
58-
setCurrentSecondaryModel: (modelConfig: ModelConfig | undefined) => void;
59-
}> = ({ modelConfig, active, setCurrentSecondaryModel }) => {
14+
isActive: boolean;
15+
onToggle: () => void;
16+
}> = ({ modelConfig, isActive, onToggle }) => {
6017
const [open, setOpen] = useState<boolean>(false);
6118

6219
return (
6320
<Button.Group>
6421
<Button
65-
variant={active ? "outlined" : "contained"}
66-
onClick={() => {
67-
handleToggle(active, setCurrentSecondaryModel, modelConfig);
68-
}}
22+
variant={isActive ? "outlined" : "contained"}
23+
onClick={onToggle}
6924
disabled={!!modelConfig.accessError}
7025
>
7126
{makeLabel(modelConfig)}
@@ -84,13 +39,23 @@ const SecondaryModelButton: React.FC<{
8439
};
8540

8641
const ModelSelect: React.FC<{
87-
currentPrimaryModel?: ModelConfig;
88-
setCurrentPrimaryModel: (model: ModelConfig | undefined) => void;
89-
currentSecondaryModel?: ModelConfig;
90-
setCurrentSecondaryModel: (model: ModelConfig | undefined) => void;
91-
}> = ({ currentPrimaryModel, currentSecondaryModel, setCurrentPrimaryModel, setCurrentSecondaryModel }) => {
42+
selectedModels: ModelConfig[];
43+
setSelectedModels: (models: ModelConfig[]) => void;
44+
}> = ({ selectedModels, setSelectedModels }) => {
9245
const { models, error, isLoading } = useAvailableModels();
9346

47+
const handleToggle = (modelConfig: ModelConfig) => {
48+
const index = selectedModels.findIndex((m) => m.modelId === modelConfig.modelId);
49+
if (index !== -1) {
50+
// Deselect the model
51+
setSelectedModels(selectedModels.filter((_, i) => i !== index));
52+
} else {
53+
// Replace any existing model of the same category and maintain correct order
54+
const filteredModels = selectedModels.filter((m) => m.category !== modelConfig.category);
55+
setSelectedModels(sortModelsByCategory([...filteredModels, modelConfig]));
56+
}
57+
};
58+
9459
if (isLoading) {
9560
return (
9661
<Typography variant="body_short" bold>
@@ -117,29 +82,35 @@ const ModelSelect: React.FC<{
11782
<div style={{ display: "flex", gap: "16px", flexWrap: "wrap" }}>
11883
{models
11984
.filter((model) => model.category === "Primary")
120-
.map((model, index) => (
121-
<PrimaryModelButton
122-
modelConfig={model}
123-
key={index}
124-
active={model.modelId === currentPrimaryModel?.modelId}
125-
setCurrentPrimaryModel={setCurrentPrimaryModel}
126-
/>
127-
))}
85+
.map((model, index) => {
86+
const isActive = selectedModels.some((m) => m.modelId === model.modelId);
87+
return (
88+
<ModelButton
89+
modelConfig={model}
90+
key={index}
91+
isActive={isActive}
92+
onToggle={() => handleToggle(model)}
93+
/>
94+
);
95+
})}
12896
</div>
12997
<Typography variant="h6" style={{ margin: "24px 0 8px 0" }}>
13098
Secondary Models
13199
</Typography>
132100
<div style={{ display: "flex", gap: "16px", flexWrap: "wrap" }}>
133101
{models
134102
.filter((model) => model.category === "Secondary")
135-
.map((model, index) => (
136-
<SecondaryModelButton
137-
modelConfig={model}
138-
key={index}
139-
active={model.modelId === currentSecondaryModel?.modelId}
140-
setCurrentSecondaryModel={setCurrentSecondaryModel}
141-
/>
142-
))}
103+
.map((model, index) => {
104+
const isActive = selectedModels.some((m) => m.modelId === model.modelId);
105+
return (
106+
<ModelButton
107+
modelConfig={model}
108+
key={index}
109+
isActive={isActive}
110+
onToggle={() => handleToggle(model)}
111+
/>
112+
);
113+
})}
143114
</div>
144115
</div>
145116
</>

frontend/src/components/Simulation/Results.tsx

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -65,30 +65,52 @@ const Results: React.FC<ResultsProps> = ({ simulationResults }) => {
6565
const panelTabs: string[] = [];
6666
const panelContents: React.ReactElement[] = [];
6767

68-
const hasConcentrations = Object.keys(simulationResults.results[0].concentrations).length > 0;
69-
if (hasConcentrations) {
70-
panelTabs.push("Output concentrations");
71-
panelContents.push(
72-
<Tabs.Panel>
73-
<MassBalanceError
74-
initial={simulationResults.input.concentrations}
75-
final={simulationResults.results[0].concentrations}
76-
/>
77-
78-
<BarChart aspectRatio={2} graphData={extractPlotData(simulationResults)} />
79-
80-
<ResultConcTable
81-
initialConcentrations={simulationResults.input.concentrations}
82-
finalConcentrations={simulationResults.results[0].concentrations}
83-
/>
84-
</Tabs.Panel>
85-
);
86-
}
87-
88-
for (const panel of simulationResults.results[0].panels) {
89-
panelTabs.push(getPanelName(panel));
90-
panelContents.push(getPanelContent(panel));
91-
}
68+
simulationResults.results.forEach((result, modelIndex) => {
69+
const modelId = simulationResults.input.models[modelIndex]?.modelId || `Model ${modelIndex + 1}`;
70+
const modelPrefix = simulationResults.results.length > 1 ? `${modelId}: ` : "";
71+
72+
const hasConcentrations = Object.keys(result.concentrations).length > 0;
73+
if (hasConcentrations) {
74+
panelTabs.push(`${modelPrefix}Output concentrations`);
75+
76+
// For the first model, compare with input concentrations
77+
// For subsequent models, compare with previous model's output
78+
const initialConcentrations =
79+
modelIndex === 0
80+
? simulationResults.input.concentrations
81+
: simulationResults.results[modelIndex - 1].concentrations;
82+
83+
panelContents.push(
84+
<Tabs.Panel key={`conc-${modelIndex}`}>
85+
<MassBalanceError initial={initialConcentrations} final={result.concentrations} />
86+
87+
<BarChart
88+
aspectRatio={2}
89+
graphData={extractPlotData({
90+
...simulationResults,
91+
results: [result],
92+
input: {
93+
...simulationResults.input,
94+
concentrations: initialConcentrations,
95+
},
96+
})}
97+
/>
98+
99+
<ResultConcTable
100+
initialConcentrations={initialConcentrations}
101+
finalConcentrations={result.concentrations}
102+
/>
103+
</Tabs.Panel>
104+
);
105+
}
106+
107+
for (const panel of result.panels) {
108+
panelTabs.push(`${modelPrefix}${getPanelName(panel)}`);
109+
panelContents.push(
110+
<Tabs.Panel key={`panel-${modelIndex}-${panelTabs.length}`}>{getPanelContent(panel)}</Tabs.Panel>
111+
);
112+
}
113+
});
92114

93115
return (
94116
<>

0 commit comments

Comments
 (0)