diff --git a/CLAUDE.md b/CLAUDE.md index 8c6d234..8049fa4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ An educational web app for exploring molecular binding simulations, built for th ``` binding-sim-edu/ ├── CLAUDE.md <- this file -├── CONTEXT_ORGANIZATION.md <- context/state architecture docs +├── docs/CONTEXT_ORGANIZATION.md <- context/state architecture docs ├── src/ │ ├── components/ <- UI components │ ├── hooks/ <- React hooks (incl. useSimulationContext.ts) @@ -25,8 +25,7 @@ binding-sim-edu/ ## State Architecture -Context is split into three providers — see `CONTEXT_ORGANIZATION.md` for full details: - +Context is split into three providers — see `docs/CONTEXT_ORGANIZATION.md` for full details: - **SimulariumUiContext** — page, module, section, viewport type, quiz, progression, completed modules - **SimulariumSimulationContext** — playback, controller, trajectory, concentrations, agents, handlers - **SimulariumAnalysisContext** — recorded concentrations, analysis reset diff --git a/bun.lockb b/bun.lockb index 9bd1e7a..93780db 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/App.tsx b/src/App.tsx index 307ab41..bd5e962 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -310,7 +310,7 @@ function App() { 1, [halfFilled, uniqMeasuredConcentrations], ); - const canDetermineKd = useMemo(() => { + const canDetermineConstant = useMemo(() => { return ( hasAValueAboveKd && hasAValueBelowKd && @@ -833,8 +833,12 @@ function App() { } @@ -843,7 +847,7 @@ function App() { pageContent={{ ...pageContent, nextButton: - (canDetermineKd && + (canDetermineConstant && pageContent.section === Section.Experiment) || pageContent.nextButton, @@ -920,9 +924,10 @@ function App() { timeToEquilibrium: timeToReachEquilibrium, colors: dataColors, - kd: simulationData.getKd( - currentModule, - ), + eqConstant: + simulationData.getEquilibriumConstant( + currentModule, + ), }} equilibriumFeedback={ equilibriumFeedback diff --git a/src/components/HelpPopup.tsx b/src/components/HelpPopup.tsx index a6b8010..fa098e3 100644 --- a/src/components/HelpPopup.tsx +++ b/src/components/HelpPopup.tsx @@ -4,17 +4,20 @@ import { Popover } from "antd"; interface HelpPopupProps { children: React.ReactNode; content: React.ReactNode; - initialOpen: boolean; + open?: boolean; + trigger?: "hover" | "click" | "focus"; } const HelpPopup: React.FC = ({ children, content, - initialOpen, + open, + trigger, }) => { return ( = ({ }) => { const { isPlaying, maxConcentration, getAgentColor } = useSimulariumSimulation(); - const { section, progressionElement } = useSimulariumUi(); + const { module, section, progressionElement } = useSimulariumUi(); + const { recordedConcentrations } = useSimulariumAnalysis(); + const isSliderDisabled = + module === Module.A_B_D_AB && + !recordedConcentrations.includes(0) && + section === Section.Experiment; const [width, setWidth] = useState(0); const MARGINS = 64.2; @@ -79,13 +86,14 @@ const Concentration: React.FC = ({ if (adjustableAgent === agent && !isPlaying) { return ( ); } else { diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx index f195e0f..3dfc036 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -5,14 +5,16 @@ import Slider from "../shared/Slider"; import { useSimulariumAnalysis } from "../../hooks/useSimulationContext"; import styles from "./concentration-slider.module.css"; import classNames from "classnames"; +import HelpPopup from "../HelpPopup"; interface SliderProps { - min: number; - max: number; + disabled?: boolean; initialValue: number; + max: number; + min: number; + name: string; onChange: (name: string, value: number) => void; onChangeComplete?: (name: string, value: number) => void; - name: string; } const Mark: React.FC<{ @@ -53,12 +55,13 @@ const Mark: React.FC<{ }; const ConcentrationSlider: React.FC = ({ - min, - max, + disabled, initialValue, + max, + min, + name, onChange, onChangeComplete, - name, }) => { // eslint-disable-next-line react-hooks/exhaustive-deps const disabledNumbers = [0]; @@ -75,26 +78,39 @@ const ConcentrationSlider: React.FC = ({ : index } disabledNumbers={disabledNumbers} - onMouseUp={() => onChangeComplete?.(name, index)} + onMouseUp={ + disabled + ? () => {} + : () => onChangeComplete?.(name, index) + } /> ), }; } return marks; - }, [min, max, disabledNumbers, onChangeComplete, name, stepSize]); + }, [min, max, disabledNumbers, disabled, onChangeComplete, name, stepSize]); return ( - + +
+ +
+
); }; diff --git a/src/components/main-layout/CenterPanel.tsx b/src/components/main-layout/CenterPanel.tsx index 22541d7..a8d60cc 100644 --- a/src/components/main-layout/CenterPanel.tsx +++ b/src/components/main-layout/CenterPanel.tsx @@ -2,11 +2,14 @@ import React from "react"; import ViewSwitch from "../ViewSwitch"; import EquilibriumQuestion from "../quiz-questions/EquilibriumQuestion"; import KdQuestion from "../quiz-questions/KdQuestion"; +import KiQuestion from "../quiz-questions/KiQuestion"; import styles from "./layout.module.css"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; +import { Module } from "../../types"; interface CenterPanelProps { - kd: number; - canDetermineEquilibrium: boolean; + eqConstant: number; + canDetermineConstant: boolean; overlay?: JSX.Element; } @@ -19,18 +22,29 @@ export const CenterPanelContext = React.createContext<{ }); const CenterPanel: React.FC = ({ - kd, - canDetermineEquilibrium, + eqConstant, + canDetermineConstant, overlay, }) => { const [lastOpened, setLastOpened] = React.useState(null); + const { module } = useSimulariumUi(); return ( <>
- + {module === Module.A_B_D_AB ? ( + + ) : ( + + )}
{overlay && overlay} diff --git a/src/components/main-layout/RightPanel.tsx b/src/components/main-layout/RightPanel.tsx index aaed196..3193f52 100644 --- a/src/components/main-layout/RightPanel.tsx +++ b/src/components/main-layout/RightPanel.tsx @@ -25,7 +25,7 @@ interface RightPanelProps { productConcentrations: number[]; timeToEquilibrium: number[]; colors: string[]; - kd: number; + eqConstant: number; }; equilibriumFeedback: ReactNode | string; showHelpPanel: boolean; @@ -71,7 +71,7 @@ const RightPanel: React.FC = ({ content={ "Use this plot to help determine when the reaction reaches equilibrium." } - initialOpen={showHelpPanel} + open={showHelpPanel} > = ({ x={equilibriumData.reactantConcentrations} y={equilibriumData.productConcentrations} colors={equilibriumData.colors} - kd={equilibriumData.kd} + eqConstant={equilibriumData.eqConstant} />
= ({ @@ -34,7 +34,7 @@ const EquilibriumPlot: React.FC = ({ height, width, colors, - kd, + eqConstant, }) => { const { fixedAgentStartingConcentration, @@ -44,7 +44,7 @@ const EquilibriumPlot: React.FC = ({ } = useSimulariumSimulation(); const { module } = useSimulariumUi(); const xMax = Math.max(...x); - const xAxisMax = Math.max(kd * 2, xMax * 1.1); + const xAxisMax = Math.max(eqConstant * 2, xMax * 1.1); // Calculate the best fit line for the data points const bestFit = useMemo(() => { diff --git a/src/components/quiz-questions/KiQuestion.tsx b/src/components/quiz-questions/KiQuestion.tsx new file mode 100644 index 0000000..00e2798 --- /dev/null +++ b/src/components/quiz-questions/KiQuestion.tsx @@ -0,0 +1,110 @@ +import React, { useEffect, useState } from "react"; +import { valueType } from "antd/es/statistic/utils"; +import { Flex } from "antd"; + +import QuizForm from "./QuizForm"; +import VisibilityControl from "../shared/VisibilityControl"; +import InputNumber from "../shared/InputNumber"; +import { FormState } from "./types"; +import styles from "./popup.module.css"; +import { MICRO } from "../../constants"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; +import { AB, D } from "../agent-symbols"; + +interface KiQuestionProps { + canAnswer: boolean; + ki: number; +} + +const KiQuestion: React.FC = ({ canAnswer, ki }) => { + const [selectedAnswer, setSelectedAnswer] = useState(null); + const [formState, setFormState] = useState(FormState.Clear); + + const { module, addCompletedModule } = useSimulariumUi(); + + useEffect(() => { + setSelectedAnswer(null); + setFormState(FormState.Clear); + }, [module]); + + const getSuccessMessage = (answer: number) => ( + <> + {answer} {MICRO}M{" "} + + Ki + {" "} + means that adding {answer} {MICRO}M of inhibitor reduces the + amount of formed by half. + + ); + + const handleAnswerSelection = (answer: valueType | null) => { + setSelectedAnswer(Number(answer)); + if (formState === FormState.Incorrect) { + setFormState(FormState.Clear); + } + }; + + const handleSubmit = () => { + const correctAnswer = ki; + const tolerance = 1.5; + if (selectedAnswer === null) { + return; + } + if (formState === FormState.Incorrect) { + setSelectedAnswer(null); + setFormState(FormState.Clear); + return; + } + const closeness = + Math.abs(selectedAnswer - correctAnswer) / correctAnswer; + if (closeness <= tolerance) { + setFormState(FormState.Correct); + addCompletedModule(module); + } else { + setFormState(FormState.Incorrect); + } + }; + + const formContent = ( +
+

+ You have now measured enough points to estimate the + concentration of D where inhibition reduces binding by 50% + (IC₅₀). +

+

+ If you're not sure, look at where the line crosses the 50% mark + on the Equilibrium concentration plot. +

+ + Ki = ? + + + + {MICRO}M + +
+ ); + + return ( + + + + ); +}; + +export default KiQuestion; diff --git a/src/simulation/LiveSimulationData.ts b/src/simulation/LiveSimulationData.ts index 133edf2..f94bef4 100644 --- a/src/simulation/LiveSimulationData.ts +++ b/src/simulation/LiveSimulationData.ts @@ -218,7 +218,7 @@ export default class LiveSimulation implements ISimulationData { }; }, {}); }; - getKd = (module: Module): number => { + getEquilibriumConstant = (module: Module): number => { return LiveSimulation.ESTIMATED_SOLUTIONS[module]; }; } diff --git a/src/simulation/PreComputedSimulationData.ts b/src/simulation/PreComputedSimulationData.ts index 664d8d9..6d9bbea 100644 --- a/src/simulation/PreComputedSimulationData.ts +++ b/src/simulation/PreComputedSimulationData.ts @@ -81,7 +81,7 @@ export default class PreComputedSimulationData implements ISimulationData { createAgentsFromConcentrations = (): InputAgent[] | null => { return null; }; - getKd = (): number => { + getEquilibriumConstant = (): number => { return 0; }; }