From 69c58f70b005a38333cbd94d7c6642709608296a Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 17 Feb 2026 13:51:34 -0800 Subject: [PATCH 01/23] equ plot shows Ki --- src/App.tsx | 74 +++++++++--------- src/components/plots/EquilibriumPlot.tsx | 82 ++++++++++++++++---- src/components/quiz-questions/KdQuestion.tsx | 4 + src/content/LowAffinity.tsx | 2 +- src/simulation/LiveSimulationData.ts | 8 +- src/simulation/PreComputedSimulationData.ts | 4 +- 6 files changed, 116 insertions(+), 58 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cfc5eb7..48baf79 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -62,7 +62,7 @@ function App() { const [time, setTime] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [trajectoryStatus, setTrajectoryStatus] = useState( - TrajectoryStatus.INITIAL + TrajectoryStatus.INITIAL, ); /** @@ -96,11 +96,11 @@ function App() { ], }); const [timeFactor, setTimeFactor] = useState( - LiveSimulationData.INITIAL_TIME_FACTOR + LiveSimulationData.INITIAL_TIME_FACTOR, ); const [completedModules, setCompletedModules] = useState>( - new Set() + new Set(), ); const [viewportSize, setViewportSize] = useState(DEFAULT_VIEWPORT_SIZE); const adjustableAgentName = @@ -161,6 +161,7 @@ function App() { const clearAllAnalysisState = useCallback(() => { resetCurrentRunAnalysisState(); setRecordedInputConcentration([]); + setProductEquilibriumConcentrations([]); setProductOverTimeTraces([]); setRecordedReactantConcentration([]); setTimeToReachEquilibrium([]); @@ -176,7 +177,7 @@ function App() { ) { isPassedEquilibrium.current = isSlopeZero( currentProductConcentrationArray, - timeFactor + timeFactor, ); } else if (arrayLength === 0 && isPassedEquilibrium.current) { isPassedEquilibrium.current = false; @@ -195,29 +196,30 @@ function App() { simulationData.getInitialConcentrations( activeAgents, currentModule, - sectionType === Section.Experiment - ) + sectionType === Section.Experiment, + ), ); - resetCurrentRunAnalysisState(); + clearAllAnalysisState(); const trajectory = simulationData.createAgentsFromConcentrations( activeAgents, currentModule, - sectionType === Section.Experiment + sectionType === Section.Experiment, ); if (!trajectory) { return null; } const longestAxis = Math.max(viewportSize.width, viewportSize.height); const startMixed = sectionType !== Section.Introduction; + console.log("NEW BINDING SIMULATOR"); return new BindingSimulator( trajectory, longestAxis / 3, - startMixed ? InitialCondition.RANDOM : InitialCondition.SORTED + startMixed ? InitialCondition.RANDOM : InitialCondition.SORTED, ); }, [ simulationData, currentModule, - resetCurrentRunAnalysisState, + clearAllAnalysisState, viewportSize.width, viewportSize.height, sectionType, @@ -238,7 +240,7 @@ function App() { { clientSimulator: clientSimulator, }, - LIVE_SIMULATION_NAME + LIVE_SIMULATION_NAME, ); }, [simulariumController, clientSimulator]); @@ -287,13 +289,13 @@ function App() { () => uniqMeasuredConcentrations.filter((c) => c > halfFilled).length >= 1, - [halfFilled, uniqMeasuredConcentrations] + [halfFilled, uniqMeasuredConcentrations], ); const hasAValueBelowKd = useMemo( () => uniqMeasuredConcentrations.filter((c) => c < halfFilled).length >= 1, - [halfFilled, uniqMeasuredConcentrations] + [halfFilled, uniqMeasuredConcentrations], ); const canDetermineKd = useMemo(() => { return ( @@ -315,17 +317,17 @@ function App() { setCurrentProductConcentrationArray([]); } }, - [currentProductConcentrationArray, productOverTimeTraces] + [currentProductConcentrationArray, productOverTimeTraces], ); const setExperiment = () => { setIsPlaying(false); - + setCurrentView(ViewType.Simulation); const activeAgents = simulationData.getActiveAgents(currentModule); const concentrations = simulationData.getInitialConcentrations( activeAgents, currentModule, - true + true, ); clientSimulator?.mixAgents(); setTimeFactor(LiveSimulationData.INITIAL_TIME_FACTOR); @@ -362,7 +364,7 @@ function App() { value, sectionType === Section.Experiment ? InitialCondition.RANDOM - : InitialCondition.SORTED + : InitialCondition.SORTED, ); simulariumController.gotoTime(1); // the number isn't used, but it triggers the update const previousConcentration = inputConcentration[agentName] || 0; @@ -376,15 +378,17 @@ function App() { addProductionTrace, resetCurrentRunAnalysisState, sectionType, - ] + ], ); + + // takes you to the home state const totalReset = useCallback(() => { setCurrentView(ViewType.Lab); const activeAgents = [AgentName.A, AgentName.B]; setCurrentModule(Module.A_B_AB); const concentrations = simulationData.getInitialConcentrations( activeAgents, - Module.A_B_AB + Module.A_B_AB, ); setLiveConcentration({ [AgentName.A]: concentrations[AgentName.A], @@ -400,7 +404,7 @@ function App() { concentrations[AgentName.B] ?? LiveSimulationData.INITIAL_CONCENTRATIONS[Module.A_B_AB][ AgentName.B - ] + ], ); setIsPlaying(false); clearAllAnalysisState(); @@ -428,7 +432,7 @@ function App() { currentProductConcentrationArray.length > 1, () => { totalReset(); - } + }, ); const hasRecordedFirstValue = useRef(false); // they have recorded a single value, changed the slider and pressed play @@ -444,7 +448,7 @@ function App() { () => { hasRecordedFirstValue.current = true; setPage(page + 1); - } + }, ); const switchToLiveSimulation = useCallback( @@ -462,7 +466,7 @@ function App() { setTrajectoryName(LIVE_SIMULATION_NAME); } }, - [simulariumController, trajectoryStatus] + [simulariumController, trajectoryStatus], ); // handle trajectory changes based on content changes @@ -488,7 +492,7 @@ function App() { await fetch3DTrajectory( url, simulariumController, - setPreComputedTrajectoryPlotData + setPreComputedTrajectoryPlotData, ); setTrajectoryStatus(TrajectoryStatus.LOADED); }; @@ -555,7 +559,7 @@ function App() { simulariumController.setCameraType(false); setTimeFactor(trajectoryInfo.timeStepSize); setFinalTime( - trajectoryInfo.totalSteps * trajectoryInfo.timeStepSize + trajectoryInfo.totalSteps * trajectoryInfo.timeStepSize, ); } }; @@ -582,7 +586,7 @@ function App() { preComputedPlotDataManager.getCurrentConcentrations(); } else if (clientSimulator) { concentrations = clientSimulator.getCurrentConcentrations( - productName + productName, ) as CurrentConcentration; } const productConcentration = concentrations[productName]; @@ -607,7 +611,7 @@ function App() { const handleFinishInputConcentrationChange = ( name: string, - value: number + value: number, ) => { // this is called when the user finishes dragging the slider // it stores the previous collected data and resets the live data @@ -631,7 +635,7 @@ function App() { const handleSwitchView = () => { setCurrentView((prevView) => - prevView === ViewType.Lab ? ViewType.Simulation : ViewType.Lab + prevView === ViewType.Lab ? ViewType.Simulation : ViewType.Lab, ); }; @@ -659,43 +663,43 @@ function App() { const currentTime = indexToTime( currentProductConcentrationArray.length, timeFactor, - simulationData.timeUnit + simulationData.timeUnit, ); const { newArray, index } = insertValueSorted( recordedReactantConcentrations, - reactantConcentration + reactantConcentration, ); setRecordedReactantConcentration(newArray); updateArrayInState( productEquilibriumConcentrations, index, productConcentration, - setProductEquilibriumConcentrations + setProductEquilibriumConcentrations, ); updateArrayInState( recordedInputConcentration, index, currentInputConcentration, - setRecordedInputConcentration + setRecordedInputConcentration, ); updateArrayInState( timeToReachEquilibrium, index, currentTime, - setTimeToReachEquilibrium + setTimeToReachEquilibrium, ); const color = PLOT_COLORS[ getColorIndex( currentInputConcentration, - simulationData.getMaxConcentration(currentModule) + simulationData.getMaxConcentration(currentModule), ) ]; updateArrayInState(dataColors, index, color, setDataColors); setEquilibriumFeedbackTimeout( <> Great! - + , ); }; diff --git a/src/components/plots/EquilibriumPlot.tsx b/src/components/plots/EquilibriumPlot.tsx index 530821d..42c12a8 100644 --- a/src/components/plots/EquilibriumPlot.tsx +++ b/src/components/plots/EquilibriumPlot.tsx @@ -9,11 +9,12 @@ import { GRAY_COLOR, } from "./constants"; import { SimulariumContext } from "../../simulation/context"; -import { AGENT_A_COLOR } from "../../constants/colors"; +import { AGENT_A_COLOR, AGENT_AB_COLOR } from "../../constants/colors"; import { MICRO } from "../../constants"; import plotStyles from "./plots.module.css"; import { Dash } from "plotly.js"; +import { Module } from "../../types"; interface PlotProps { x: number[]; @@ -37,6 +38,7 @@ const EquilibriumPlot: React.FC = ({ productName, getAgentColor, adjustableAgentName, + module, } = useContext(SimulariumContext); const xMax = Math.max(...x); const xAxisMax = Math.max(kd * 2, xMax * 1.1); @@ -47,17 +49,35 @@ const EquilibriumPlot: React.FC = ({ xVal, y[index], ]); + let bestFit; + let value; + if (module === Module.A_B_D_AB) { + bestFit = regression.exponential(regressionData); + const max = Math.max(...y); + const min = Math.min(...y); + const halfMax = (max - min) / 2 + min; + // for exponential, the equation is in the form y = a * e^(b*x) + // bestFit.equation[0] is a and bestFit.equation[1] is b, so to solve for x when y is halfMax: + // halfMax = a * e^(b*x) + // halfMax / a = e^(b*x) + // ln(halfMax / a) = b*x + // x = ln(halfMax / a) / b + value = + Math.log(halfMax / bestFit.equation[0]) / bestFit.equation[1]; + } else { + bestFit = regression.logarithmic(regressionData); - const bestFit = regression.logarithmic(regressionData); + const halfFilled = fixedAgentStartingConcentration / 2; + value = + Math.E ** + ((halfFilled - bestFit.equation[0]) / bestFit.equation[1]); + } const bestFitPoints = bestFit.points; + const bestFitX = bestFitPoints.map((point) => point[0]); const bestFitY = bestFitPoints.map((point) => point[1]); - const halfFilled = fixedAgentStartingConcentration / 2; - const kdValue = - Math.E ** - ((halfFilled - bestFit.equation[0]) / bestFit.equation[1]); - return { x: bestFitX, y: bestFitY, kd: kdValue }; - }, [x, y, fixedAgentStartingConcentration]); + return { x: bestFitX, y: bestFitY, value: value }; + }, [x, y, fixedAgentStartingConcentration, module]); const hintOverlay = (
= ({ dash: "dot" as Dash, }; - const horizontalLine = { + const kdHorizontalLine = { x: [0, xAxisMax], - y: [5, 5], + y: [ + fixedAgentStartingConcentration / 2, + fixedAgentStartingConcentration / 2, + ], mode: "lines", name: "50% bound", hovertemplate: "50% bound", @@ -94,22 +117,44 @@ const EquilibriumPlot: React.FC = ({ }, line: lineOptions, }; - const horizontalLineMax = { + const kdHorizontalLineMax = { x: [0, xAxisMax], - y: [10, 10], + y: [fixedAgentStartingConcentration, fixedAgentStartingConcentration], mode: "lines", name: "Initial [A]", hoverlabel: { bgcolor: AGENT_A_COLOR }, hovertemplate: "Initial [A]", line: lineOptions, }; + const kiHorizontalLine = { + x: [0, xAxisMax], + y: [Math.max(...y) / 2, Math.max(...y) / 2], + mode: "lines", + name: "Half max inhibition", + hovertemplate: "50% inhibition", + hoverlabel: { + bgcolor: AGENT_A_COLOR, + }, + line: lineOptions, + }; + const kiHorizontalLineMax = { + x: [0, xAxisMax], + y: [Math.max(...y), Math.max(...y)], + mode: "lines", + name: "[AB] without inhibitor", + hoverlabel: { bgcolor: AGENT_AB_COLOR }, + line: lineOptions, + }; const kdIndicator = { - x: [bestFit.kd, bestFit.kd], + x: [bestFit.value, bestFit.value], y: [0, fixedAgentStartingConcentration / 2], mode: "lines", name: "", - hovertemplate: `Kd: ${bestFit.kd.toFixed(2)} ${MICRO}M`, + hovertemplate: + module === Module.A_B_D_AB + ? `Ki: ${bestFit.value.toFixed(2)} ${MICRO}M` + : `Kd: ${bestFit.value.toFixed(2)} ${MICRO}M`, hoverlabel: { bgcolor: getAgentColor(adjustableAgentName), }, @@ -119,6 +164,11 @@ const EquilibriumPlot: React.FC = ({ dash: "dot" as Dash, }, }; + + const horizontalLine = + module === Module.A_B_D_AB ? kiHorizontalLine : kdHorizontalLine; + const horizontalLineMax = + module === Module.A_B_D_AB ? kiHorizontalLineMax : kdHorizontalLineMax; const traces = [ horizontalLine, horizontalLineMax, @@ -158,7 +208,7 @@ const EquilibriumPlot: React.FC = ({ traces.push(kdIndicator); // filter out axis values that are so close to the kd value that they would overlap on the axis xAxisTicks = xAxisTicks.filter( - (tick) => Math.abs(tick - bestFit.kd) >= interval / 2 + (tick) => Math.abs(tick - bestFit.value) >= interval / 2, ); } @@ -176,7 +226,7 @@ const EquilibriumPlot: React.FC = ({ color: getAgentColor(adjustableAgentName), }, tickmode: bestFitVisible ? ("array" as const) : ("auto" as const), - tickvals: [...xAxisTicks, bestFit.kd.toFixed(1)], + tickvals: [...xAxisTicks, bestFit.value.toFixed(1)], }, yaxis: { ...AXIS_SETTINGS, diff --git a/src/components/quiz-questions/KdQuestion.tsx b/src/components/quiz-questions/KdQuestion.tsx index 7be9859..ed09c26 100644 --- a/src/components/quiz-questions/KdQuestion.tsx +++ b/src/components/quiz-questions/KdQuestion.tsx @@ -94,6 +94,10 @@ const KdQuestion: React.FC = ({ kd, canAnswer }) => { You have now measured enough points to estimate the value of B where half of the binding sites of A are occupied.

+

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

Kd = ? diff --git a/src/content/LowAffinity.tsx b/src/content/LowAffinity.tsx index 665e9ac..9a909e3 100644 --- a/src/content/LowAffinity.tsx +++ b/src/content/LowAffinity.tsx @@ -61,7 +61,7 @@ export const lowAffinityContentArray: PageContent[] = [ content: "Congratulations, you’ve completed the Low Affinity experiment!", backButton: true, - // nextButton: true, + nextButton: true, nextButtonText: "View examples", section: Section.BonusContent, layout: LayoutType.FullScreenOverlay, diff --git a/src/simulation/LiveSimulationData.ts b/src/simulation/LiveSimulationData.ts index 2bddcb1..133edf2 100644 --- a/src/simulation/LiveSimulationData.ts +++ b/src/simulation/LiveSimulationData.ts @@ -36,7 +36,7 @@ const agentB: InputAgent = { initialConcentration: 0, radius: 1, partners: [0], - kOn: 0.9, + kOn: 0.95, kOff: 0.01, color: AGENT_B_COLOR, complexColor: AGENT_AB_COLOR, @@ -163,7 +163,7 @@ export default class LiveSimulation implements ISimulationData { createAgentsFromConcentrations = ( activeAgents?: AgentName[], module?: Module, - isExperiment: boolean = false + isExperiment: boolean = false, ): InputAgent[] => { if (!module) { throw new Error("Module must be specified to create agents."); @@ -174,7 +174,7 @@ export default class LiveSimulation implements ISimulationData { const concentrations = this.getInitialConcentrations( activeAgents, module, - isExperiment + isExperiment, ); return (activeAgents ?? []).map((agentName: AgentName) => { const agent = { @@ -206,7 +206,7 @@ export default class LiveSimulation implements ISimulationData { getInitialConcentrations = ( activeAgents: AgentName[], module: Module, - isExperiment: boolean = false + isExperiment: boolean = false, ): CurrentConcentration => { const concentrations = isExperiment ? { ...LiveSimulation.EXPERIMENT_CONCENTRATIONS[module] } diff --git a/src/simulation/PreComputedSimulationData.ts b/src/simulation/PreComputedSimulationData.ts index 3b5e6e7..664d8d9 100644 --- a/src/simulation/PreComputedSimulationData.ts +++ b/src/simulation/PreComputedSimulationData.ts @@ -13,7 +13,7 @@ import ISimulationData, { import { MICRO } from "../constants"; export default class PreComputedSimulationData implements ISimulationData { - static NAME_TO_FUNCTION_MAP = { + static NAME_TO_TYPE_MAP = { [AgentName.Antibody]: AgentType.Fixed, [AgentName.Antigen]: AgentType.Adjustable_1, [ProductName.AntibodyAntigen]: AgentType.Complex_1, @@ -58,7 +58,7 @@ export default class PreComputedSimulationData implements ISimulationData { getAgentType = (name: AgentName | ProductName): AgentType => { return ( - PreComputedSimulationData.NAME_TO_FUNCTION_MAP as Record< + PreComputedSimulationData.NAME_TO_TYPE_MAP as Record< AgentName | ProductName, AgentType > From 96023f7af174dfa48312b0ff2b803447375039f8 Mon Sep 17 00:00:00 2001 From: meganrm Date: Mon, 9 Mar 2026 16:23:49 -0700 Subject: [PATCH 02/23] format --- src/index.css | 2 +- src/simulation/BindingSimulator2D.ts | 38 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/index.css b/src/index.css index 42511f9..fc2e684 100644 --- a/src/index.css +++ b/src/index.css @@ -36,7 +36,7 @@ body { height: 100dvh; background-color: var(--background-color); color: var(--text-color); - /* overflow: hidden; */ + overflow: hidden; } h1, diff --git a/src/simulation/BindingSimulator2D.ts b/src/simulation/BindingSimulator2D.ts index 6e488fc..49bed5e 100644 --- a/src/simulation/BindingSimulator2D.ts +++ b/src/simulation/BindingSimulator2D.ts @@ -45,7 +45,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { agents: InputAgent[], size: number, initPositions: InitialCondition = InitialCondition.SORTED, - timeFactor: number = LiveSimulationData.DEFAULT_TIME_FACTOR + timeFactor: number = LiveSimulationData.DEFAULT_TIME_FACTOR, ) { this.size = size; this.productColor = new Map(); @@ -92,7 +92,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { private getProductIdByAgents( agent1: BindingInstance | InputAgent, - agent2: BindingInstance | InputAgent + agent2: BindingInstance | InputAgent, ) { if (agent1.id > agent2.id) { return `${agent1.id}#${agent2.id}`; @@ -111,7 +111,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { const color2 = this.productColor.get(partnerId); if (color1 && color2) { throw new Error( - `Both agents (${id} and ${partnerId}) have a product color defined. Only one should have a product color.` + `Both agents (${id} and ${partnerId}) have a product color defined. Only one should have a product color.`, ); } return color2 || color1 || ""; @@ -139,7 +139,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { private initializeAgents( agents: InputAgent[], - initPositions: InitialCondition = InitialCondition.SORTED + initPositions: InitialCondition = InitialCondition.SORTED, ): StoredAgent[] { for (let i = 0; i < agents.length; ++i) { const agent = agents[i] as StoredAgent; // count is no longer optional @@ -148,7 +148,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { // the count will already be set if (agent.count === undefined) { agent.count = this.convertConcentrationToCount( - agent.initialConcentration + agent.initialConcentration, ); } if (agent.complexColor) { @@ -167,14 +167,14 @@ export default class BindingSimulator implements IClientSimulatorImpl { } const circle = new Circle( new Vector(...position), - agent.radius + agent.radius, ); const instance = new BindingInstance( circle, agent.id, agent.partners, agent.kOn, - agent.kOff + agent.kOff, ); this.system.insert(instance); this.instances.push(instance); @@ -196,7 +196,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { this.system.createLine( new Vector(point[0], point[1]), new Vector(nextPoint[0], nextPoint[1]), - { isStatic: true } + { isStatic: true }, ); }); } @@ -274,7 +274,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { public changeConcentration( agentId: number, newConcentration: number, - initPositions: InitialCondition + initPositions: InitialCondition, ) { const agent = find(this.agents, (agent) => agent.id === agentId); if (!agent) { @@ -305,21 +305,21 @@ export default class BindingSimulator implements IClientSimulatorImpl { const circle = new Circle( new Vector(...position), - agent.radius + agent.radius, ); const instance = new BindingInstance( circle, agent.id, agent.partners, agent.kOn, - agent.kOff + agent.kOff, ); this.system.insert(instance); this.instances.push(instance); } } else if (diff < 0) { const toRemove = this.instances.filter( - (instance) => instance.id === agentId + (instance) => instance.id === agentId, ); for (let i = 0; i < Math.abs(diff); ++i) { const instance = toRemove[i]; @@ -349,14 +349,14 @@ export default class BindingSimulator implements IClientSimulatorImpl { const init = <{ [key: string]: number }>{}; const concentrations = this.agents.reduce((acc, agent) => { acc[agent.name] = this.convertCountToConcentration( - agent.count - this.currentComplexMap.get(agent.id.toString())! + agent.count - this.currentComplexMap.get(agent.id.toString())!, ); return acc; }, init); const productId = this.getProductIdByProductName(product); if (productId) { concentrations[product] = this.convertCountToConcentration( - this.currentComplexMap.get(productId) || 0 + this.currentComplexMap.get(productId) || 0, ); } return concentrations; @@ -391,7 +391,7 @@ export default class BindingSimulator implements IClientSimulatorImpl { for (let i = 0; i < this.instances.length; ++i) { const releasedChild = this.instances[i].oneStep( this.size, - this.timeFactor + this.timeFactor, ); if (releasedChild) { this.currentNumberOfUnbindingEvents++; @@ -412,10 +412,10 @@ export default class BindingSimulator implements IClientSimulatorImpl { const childPosition = instance.pos; const distanceVector = new Vector( childPosition.x - parentPosition.x, - childPosition.y - parentPosition.y + childPosition.y - parentPosition.y, ); const distance = Math.sqrt( - distanceVector.x ** 2 + distanceVector.y ** 2 + distanceVector.x ** 2 + distanceVector.y ** 2, ); const perfectBoundDistance = instance.parent.r + instance.r - bindingOverlap; @@ -433,12 +433,12 @@ export default class BindingSimulator implements IClientSimulatorImpl { private incrementBoundCounts( a: BindingInstance, b: BindingInstance, - amount: number + amount: number, ) { const complexName = this.getProductIdByAgents(a, b); this.currentComplexMap.set( complexName, - (this.currentComplexMap.get(complexName) || 0) + amount + (this.currentComplexMap.get(complexName) || 0) + amount, ); const previousValueA = this.currentComplexMap.get(a.id.toString()) || 0; From ed462c237e41f37fd36f6283db7d43aaf52f52ef Mon Sep 17 00:00:00 2001 From: meganrm Date: Mon, 9 Mar 2026 16:43:58 -0700 Subject: [PATCH 03/23] separate context --- src/App.tsx | 517 ++++++++++-------- src/components/AdminUi.tsx | 5 +- src/components/LabView.tsx | 10 +- src/components/MixButton.tsx | 4 +- src/components/PageIndicator.tsx | 6 +- src/components/PlayButton.tsx | 4 +- src/components/ScaleBar.tsx | 4 +- src/components/StartExperiment.tsx | 4 +- src/components/ViewSwitch.tsx | 14 +- src/components/Viewer.tsx | 9 +- .../concentration-display/Concentration.tsx | 24 +- .../ConcentrationSlider.tsx | 4 +- .../LiveConcentrationDisplay.tsx | 6 +- .../main-layout/ContentPanelTimer.tsx | 12 +- src/components/main-layout/NavPanel.tsx | 4 +- src/components/main-layout/RightPanel.tsx | 4 +- src/components/plots/EquilibriumPlot.tsx | 9 +- src/components/plots/EventsOverTimePlot.tsx | 10 +- .../plots/ProductConcentrationPlot.tsx | 6 +- .../quiz-questions/EquilibriumQuestion.tsx | 4 +- src/components/quiz-questions/KdQuestion.tsx | 4 +- src/components/shared/BackButton.tsx | 4 +- src/components/shared/NextButton.tsx | 5 +- src/components/shared/ProgressionControl.tsx | 7 +- src/components/shared/VisibilityControl.tsx | 4 +- src/simulation/context.tsx | 70 ++- 26 files changed, 439 insertions(+), 315 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 48baf79..fc2e1eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,9 +33,17 @@ import RightPanel from "./components/main-layout/RightPanel"; import ReactionDisplay from "./components/main-layout/ReactionDisplay"; import ContentPanelTimer from "./components/main-layout/ContentPanelTimer"; import content, { FIRST_PAGE, moduleNames } from "./content"; -import { DEFAULT_VIEWPORT_SIZE, LIVE_SIMULATION_NAME } from "./constants"; +import { + DEFAULT_VIEWPORT_SIZE, + LIVE_SIMULATION_NAME, + ProgressionElement, +} from "./constants"; import CenterPanel from "./components/main-layout/CenterPanel"; -import { SimulariumContext } from "./simulation/context"; +import { + SimulariumAnalysisContext, + SimulariumSimulationContext, + SimulariumUiContext, +} from "./simulation/context"; import NavPanel from "./components/main-layout/NavPanel"; import AdminUI from "./components/AdminUi"; import { ProductOverTimeTrace } from "./components/plots/types"; @@ -320,7 +328,7 @@ function App() { [currentProductConcentrationArray, productOverTimeTraces], ); - const setExperiment = () => { + const setExperiment = useCallback(() => { setIsPlaying(false); setCurrentView(ViewType.Simulation); const activeAgents = simulationData.getActiveAgents(currentModule); @@ -333,7 +341,7 @@ function App() { setTimeFactor(LiveSimulationData.INITIAL_TIME_FACTOR); setInputConcentration(concentrations); setLiveConcentration(concentrations); - }; + }, [simulationData, currentModule, clientSimulator]); const handleMixAgents = useCallback(() => { if (clientSimulator) { @@ -510,7 +518,8 @@ function App() { clearAllAnalysisState, ]); - const { section } = content[currentModule][page]; + const pageContent = content[currentModule][page]; + const { section } = pageContent; useEffect(() => { if (section === Section.Experiment) { setTimeFactor(LiveSimulationData.DEFAULT_TIME_FACTOR); @@ -521,93 +530,112 @@ function App() { // User input handlers - const addCompletedModule = (module: Module) => { + const addCompletedModule = useCallback((module: Module) => { setCompletedModules((prev: Set) => new Set(prev).add(module)); - }; + }, []); - const setModule = (module: Module) => { - setPage(FIRST_PAGE[module]); - clearAllAnalysisState(); - setCurrentModule(module); - setIsPlaying(false); - // the first module is the only one that starts with the lab view - if (module === Module.A_B_AB) { - setCurrentView(ViewType.Lab); - } else { - setCurrentView(ViewType.Simulation); - } - }; + const setModule = useCallback( + (module: Module) => { + setPage(FIRST_PAGE[module]); + clearAllAnalysisState(); + setCurrentModule(module); + setIsPlaying(false); + // the first module is the only one that starts with the lab view + if (module === Module.A_B_AB) { + setCurrentView(ViewType.Lab); + } else { + setCurrentView(ViewType.Simulation); + } + }, + [clearAllAnalysisState], + ); - const handleStartExperiment = () => { + const handleStartExperiment = useCallback(() => { clearAllAnalysisState(); setExperiment(); setPage(page + 1); - }; + }, [clearAllAnalysisState, page, setExperiment]); // trigger when the trajectory data has been sent by the viewer - const handleTrajectoryChange = (trajectoryInfo: TrajectoryFileInfo) => { - setTrajectoryName(trajectoryInfo.trajectoryTitle || ""); - if (trajectoryInfo.trajectoryTitle === LIVE_SIMULATION_NAME) { - // 2d trajectory - // switch to orthographic camera - simulariumController.setCameraType(true); - setPreComputedTrajectoryPlotData(undefined); - setFinalTime(-1); - } else { - // 3d trajectory - // switch to perspective camera - simulariumController.setCameraType(false); - setTimeFactor(trajectoryInfo.timeStepSize); - setFinalTime( - trajectoryInfo.totalSteps * trajectoryInfo.timeStepSize, - ); - } - }; + const handleTrajectoryChange = useCallback( + (trajectoryInfo: TrajectoryFileInfo) => { + setTrajectoryName(trajectoryInfo.trajectoryTitle || ""); + if (trajectoryInfo.trajectoryTitle === LIVE_SIMULATION_NAME) { + // 2d trajectory + // switch to orthographic camera + simulariumController.setCameraType(true); + setPreComputedTrajectoryPlotData(undefined); + setFinalTime(-1); + } else { + // 3d trajectory + // switch to perspective camera + simulariumController.setCameraType(false); + setTimeFactor(trajectoryInfo.timeStepSize); + setFinalTime( + trajectoryInfo.totalSteps * trajectoryInfo.timeStepSize, + ); + } + }, + [simulariumController], + ); - const handleTimeChange = (timeData: TimeData) => { - const { time } = timeData; - setTime(time); - // can't use isLastFrame here because the time is not updated - // in state yet - if (finalTime > 0 && time >= finalTime - timeFactor && isPlaying) { - setIsPlaying(false); - } - let concentrations: CurrentConcentration = {}; - let previousData = currentProductConcentrationArray; - - if (preComputedPlotDataManager) { - if (timeData.time === 0) { - // for the 3D trajectory, - // we want to reset the data when we loop - previousData = []; + const handleTimeChange = useCallback( + (timeData: TimeData) => { + const { time } = timeData; + setTime(time); + // can't use isLastFrame here because the time is not updated + // in state yet + if (finalTime > 0 && time >= finalTime - timeFactor && isPlaying) { + setIsPlaying(false); } - preComputedPlotDataManager.update(timeData.time); - concentrations = - preComputedPlotDataManager.getCurrentConcentrations(); - } else if (clientSimulator) { - concentrations = clientSimulator.getCurrentConcentrations( - productName, - ) as CurrentConcentration; - } - const productConcentration = concentrations[productName]; - if (productConcentration !== undefined) { - const newData = [...previousData, productConcentration]; - setCurrentProductConcentrationArray(newData); - } - setLiveConcentration(concentrations); - if (timeData.time % 10 === 0 && clientSimulator) { - const { numberBindEvents, numberUnBindEvents } = - clientSimulator.getEvents(); - setBindingEventsOverTime([ - ...bindingEventsOverTime, - numberBindEvents, - ]); - setUnBindingEventsOverTime([ - ...unBindingEventsOverTime, - numberUnBindEvents, - ]); - } - }; + let concentrations: CurrentConcentration = {}; + let previousData = currentProductConcentrationArray; + + if (preComputedPlotDataManager) { + if (timeData.time === 0) { + // for the 3D trajectory, + // we want to reset the data when we loop + previousData = []; + } + preComputedPlotDataManager.update(timeData.time); + concentrations = + preComputedPlotDataManager.getCurrentConcentrations(); + } else if (clientSimulator) { + concentrations = clientSimulator.getCurrentConcentrations( + productName, + ) as CurrentConcentration; + } + const productConcentration = concentrations[productName]; + if (productConcentration !== undefined) { + const newData = [...previousData, productConcentration]; + setCurrentProductConcentrationArray(newData); + } + setLiveConcentration(concentrations); + if (timeData.time % 10 === 0 && clientSimulator) { + const { numberBindEvents, numberUnBindEvents } = + clientSimulator.getEvents(); + setBindingEventsOverTime([ + ...bindingEventsOverTime, + numberBindEvents, + ]); + setUnBindingEventsOverTime([ + ...unBindingEventsOverTime, + numberUnBindEvents, + ]); + } + }, + [ + finalTime, + timeFactor, + isPlaying, + currentProductConcentrationArray, + preComputedPlotDataManager, + clientSimulator, + productName, + bindingEventsOverTime, + unBindingEventsOverTime, + ], + ); const handleFinishInputConcentrationChange = ( name: string, @@ -633,11 +661,11 @@ function App() { }, 3000); }; - const handleSwitchView = () => { + const handleSwitchView = useCallback(() => { setCurrentView((prevView) => prevView === ViewType.Lab ? ViewType.Simulation : ViewType.Lab, ); - }; + }, []); const handleRecordEquilibrium = () => { if (!clientSimulator) { @@ -706,148 +734,207 @@ function App() { const { totalMainContentPages } = useModule(currentModule); const lastPageOfExperiment = page === totalMainContentPages; + const uiContextValue = useMemo( + () => ({ + addCompletedModule, + completedModules, + module: currentModule, + page, + progressionElement: (pageContent.progressionElement ?? + "") as ProgressionElement, + quizQuestion: pageContent.quizQuestion || "", + resetAllState: totalReset, + section: pageContent.section, + setModule, + setPage, + setViewportType: handleSwitchView, + viewportType: currentView, + }), + [ + addCompletedModule, + completedModules, + currentModule, + currentView, + handleSwitchView, + page, + pageContent, + setModule, + setPage, + totalReset, + ], + ); + + const simulationContextValue = useMemo( + () => ({ + adjustableAgentName, + currentProductionConcentration: liveConcentration[productName] || 0, + fixedAgentStartingConcentration: + inputConcentration[AgentName.A] || 0, + getAgentColor: simulationData.getAgentColor, + handleMixAgents, + handleStartExperiment, + handleTimeChange, + handleTrajectoryChange, + isPlaying, + maxConcentration: simulationData.getMaxConcentration(currentModule), + productName, + setIsPlaying, + setViewportSize, + simulariumController, + timeFactor, + timeUnit: simulationData.timeUnit, + trajectoryName, + viewportSize, + }), + [ + adjustableAgentName, + currentModule, + handleMixAgents, + handleStartExperiment, + handleTimeChange, + handleTrajectoryChange, + inputConcentration, + isPlaying, + liveConcentration, + productName, + setIsPlaying, + setViewportSize, + simulariumController, + simulationData, + timeFactor, + trajectoryName, + viewportSize, + ], + ); + + const analysisContextValue = useMemo( + () => ({ + recordedConcentrations: recordedInputConcentration, + resetAnalysisState: clearAllAnalysisState, + }), + [clearAllAnalysisState, recordedInputConcentration], + ); + return ( <>
- - - } - content={ - - } - header={ - - } - landingPage={ - - } - leftPanel={ - + + + } - handleFinishInputConcentrationChange={ - handleFinishInputConcentrationChange + content={ + } - bindingEventsOverTime={bindingEventsOverTime} - unbindingEventsOverTime={ - unBindingEventsOverTime + header={ + } - adjustableAgent={adjustableAgentName} - /> - } - reactionPanel={ - - } - rightPanel={ - } - productOverTimeTraces={productOverTimeTraces} - currentProductConcentrationArray={ - currentProductConcentrationArray + leftPanel={ + } - handleRecordEquilibrium={ - handleRecordEquilibrium + reactionPanel={ + } - currentAdjustableAgentConcentration={ - inputConcentration[adjustableAgentName] || 0 + rightPanel={ + } - equilibriumData={{ - inputConcentrations: - recordedInputConcentration, - reactantConcentrations: - recordedReactantConcentrations, - productConcentrations: - productEquilibriumConcentrations, - timeToEquilibrium: timeToReachEquilibrium, - colors: dataColors, - kd: simulationData.getKd(currentModule), - }} - equilibriumFeedback={equilibriumFeedback} + section={pageContent.section} + layout={pageContent.layout} + /> + - } - section={content[currentModule][page].section} - layout={content[currentModule][page].layout} - /> - - + + +
); diff --git a/src/components/AdminUi.tsx b/src/components/AdminUi.tsx index 93bf59c..7885257 100644 --- a/src/components/AdminUi.tsx +++ b/src/components/AdminUi.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect } from "react"; import Slider from "./shared/Slider"; import { BG_DARK, LIGHT_GREY } from "../constants/colors"; -import { SimulariumContext } from "../simulation/context"; +import { SimulariumUiContext } from "../simulation/context"; import { InputNumber, SliderSingleProps } from "antd"; import { zStacking } from "../constants/z-stacking"; import { Module } from "../types"; @@ -18,7 +18,8 @@ const AdminUI: React.FC = ({ setTimeFactor, totalPages, }) => { - const { page, setPage, module, setModule } = useContext(SimulariumContext); + const { page, setPage, module, setModule } = + useContext(SimulariumUiContext); const [visible, setVisible] = React.useState(false); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { diff --git a/src/components/LabView.tsx b/src/components/LabView.tsx index 8137709..b044c49 100644 --- a/src/components/LabView.tsx +++ b/src/components/LabView.tsx @@ -1,6 +1,9 @@ import Rainbow from "rainbowvis.js"; import { useContext, useMemo } from "react"; -import { SimulariumContext } from "../simulation/context"; +import { + SimulariumSimulationContext, + SimulariumUiContext, +} from "../simulation/context"; import Cuvette from "./icons/Cuvette"; import styles from "./labview.module.css"; import classNames from "classnames"; @@ -12,11 +15,10 @@ const LabView: React.FC = () => { const { currentProductionConcentration, maxConcentration, - page, getAgentColor, productName, - module, - } = useContext(SimulariumContext); + } = useContext(SimulariumSimulationContext); + const { page, module } = useContext(SimulariumUiContext); const color = getAgentColor(productName); const colorGradient = useMemo(() => { const rainbow = new Rainbow(); diff --git a/src/components/MixButton.tsx b/src/components/MixButton.tsx index 430448d..f683202 100644 --- a/src/components/MixButton.tsx +++ b/src/components/MixButton.tsx @@ -1,13 +1,13 @@ import React, { useContext } from "react"; -import { SimulariumContext } from "../simulation/context"; +import { SimulariumSimulationContext } from "../simulation/context"; import { TertiaryButton } from "./shared/ButtonLibrary"; import { MIX_AGENTS_ID } from "../constants"; import ProgressionControl from "./shared/ProgressionControl"; import style from "./start-experiment.module.css"; const MixButton: React.FC = () => { - const { handleMixAgents } = useContext(SimulariumContext); + const { handleMixAgents } = useContext(SimulariumSimulationContext); return ( diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index aa27216..118cb28 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -5,7 +5,7 @@ import classNames from "classnames"; import { moduleNames } from "../content"; import styles from "./page-indicator.module.css"; -import { SimulariumContext } from "../simulation/context"; +import { SimulariumUiContext } from "../simulation/context"; interface PageIndicatorProps { title: string; @@ -19,7 +19,7 @@ const PageIndicator: React.FC = ({ total, }) => { const { module, setModule, completedModules } = - React.useContext(SimulariumContext); + React.useContext(SimulariumUiContext); const indexOfActiveModule: number = useMemo(() => { let toReturn = -1; map(moduleNames, (name, index) => { @@ -86,7 +86,7 @@ const PageIndicator: React.FC = ({ size={["100%", 4]} percent={getModulePercent( isActiveModule, - moduleIndex + moduleIndex, )} showInfo={false} /> diff --git a/src/components/PlayButton.tsx b/src/components/PlayButton.tsx index e29bc8b..28bc009 100644 --- a/src/components/PlayButton.tsx +++ b/src/components/PlayButton.tsx @@ -1,7 +1,7 @@ import React, { useContext } from "react"; import { CaretRightOutlined, PauseOutlined } from "@ant-design/icons"; -import { SimulariumContext } from "../simulation/context"; +import { SimulariumSimulationContext } from "../simulation/context"; import ProgressionControl from "./shared/ProgressionControl"; import VisibilityControl from "./shared/VisibilityControl"; import { OverlayButton } from "./shared/ButtonLibrary"; @@ -9,7 +9,7 @@ import { Module } from "../types"; import { PLAY_BUTTON_ID } from "../constants"; const PlayButton: React.FC = () => { - const { isPlaying, setIsPlaying } = useContext(SimulariumContext); + const { isPlaying, setIsPlaying } = useContext(SimulariumSimulationContext); const handleClick = () => { setIsPlaying(!isPlaying); diff --git a/src/components/ScaleBar.tsx b/src/components/ScaleBar.tsx index 7300644..1bc3fa5 100644 --- a/src/components/ScaleBar.tsx +++ b/src/components/ScaleBar.tsx @@ -2,14 +2,14 @@ import React, { useContext } from "react"; import styles from "./scalebar.module.css"; import { MICRO } from "../constants"; -import { SimulariumContext } from "../simulation/context"; +import { SimulariumSimulationContext } from "../simulation/context"; interface ScaleBarProps { productColor: string; } const ScaleBar: React.FC = ({ productColor }) => { - const { maxConcentration } = useContext(SimulariumContext); + const { maxConcentration } = useContext(SimulariumSimulationContext); const labelArray = []; const interval = maxConcentration / 5; for (let i = maxConcentration; i >= 0; i = i - interval) { diff --git a/src/components/StartExperiment.tsx b/src/components/StartExperiment.tsx index 20dc924..f583731 100644 --- a/src/components/StartExperiment.tsx +++ b/src/components/StartExperiment.tsx @@ -1,13 +1,13 @@ import React, { useContext } from "react"; import classNames from "classnames"; -import { SimulariumContext } from "../simulation/context"; +import { SimulariumSimulationContext } from "../simulation/context"; import { TertiaryButton } from "./shared/ButtonLibrary"; import style from "./start-experiment.module.css"; const StartExperiment: React.FC = () => { - const { handleStartExperiment } = useContext(SimulariumContext); + const { handleStartExperiment } = useContext(SimulariumSimulationContext); const [initial, setIsInitial] = React.useState(true); const handleClick = () => { if (initial) { diff --git a/src/components/ViewSwitch.tsx b/src/components/ViewSwitch.tsx index 21d03c0..c6eec86 100644 --- a/src/components/ViewSwitch.tsx +++ b/src/components/ViewSwitch.tsx @@ -1,7 +1,10 @@ import React, { useContext } from "react"; import Viewer from "./Viewer"; -import { SimulariumContext } from "../simulation/context"; +import { + SimulariumSimulationContext, + SimulariumUiContext, +} from "../simulation/context"; import ProgressionControl from "./shared/ProgressionControl"; import PlayButton from "./PlayButton"; import { OverlayButton } from "./shared/ButtonLibrary"; @@ -14,10 +17,11 @@ import { FIRST_PAGE } from "../content"; import { VIEW_SWITCH_ID } from "../constants"; const ViewSwitch: React.FC = () => { - const { viewportType, setViewportType } = useContext(SimulariumContext); - - const { page, isPlaying, setIsPlaying, handleTimeChange, module } = - useContext(SimulariumContext); + const { viewportType, setViewportType, page, module } = + useContext(SimulariumUiContext); + const { isPlaying, setIsPlaying, handleTimeChange } = useContext( + SimulariumSimulationContext, + ); const isFirstPageOfFirstModule = page === FIRST_PAGE[module] + 1 && module === Module.A_B_AB; diff --git a/src/components/Viewer.tsx b/src/components/Viewer.tsx index 804cd2a..2260dbe 100644 --- a/src/components/Viewer.tsx +++ b/src/components/Viewer.tsx @@ -13,7 +13,10 @@ import SimulariumViewer, { } from "@aics/simularium-viewer"; import "@aics/simularium-viewer/style/style.css"; -import { SimulariumContext } from "../simulation/context"; +import { + SimulariumSimulationContext, + SimulariumUiContext, +} from "../simulation/context"; import styles from "./viewer.module.css"; import useWindowResize from "../hooks/useWindowResize"; import { LIVE_SIMULATION_NAME } from "../constants"; @@ -43,8 +46,8 @@ export default function Viewer({ handleTimeChange }: ViewerProps): ReactNode { simulariumController, handleTrajectoryChange, trajectoryName, - page, - } = useContext(SimulariumContext); + } = useContext(SimulariumSimulationContext); + const { page } = useContext(SimulariumUiContext); const setViewportToContainerSize = useCallback(() => { if (container.current) { diff --git a/src/components/concentration-display/Concentration.tsx b/src/components/concentration-display/Concentration.tsx index b288a24..2d8d2dc 100644 --- a/src/components/concentration-display/Concentration.tsx +++ b/src/components/concentration-display/Concentration.tsx @@ -10,7 +10,10 @@ import { Section, UiElement, } from "../../types"; -import { SimulariumContext } from "../../simulation/context"; +import { + SimulariumSimulationContext, + SimulariumUiContext, +} from "../../simulation/context"; import LiveConcentrationDisplay from "./LiveConcentrationDisplay"; import ConcentrationSlider from "./ConcentrationSlider"; import { MICRO, CHANGE_CONCENTRATION_ID } from "../../constants"; @@ -42,20 +45,17 @@ const Concentration: React.FC = ({ liveConcentration, onChangeComplete, }) => { - const { - isPlaying, - maxConcentration, - getAgentColor, - section, - progressionElement, - } = useContext(SimulariumContext); + const { isPlaying, maxConcentration, getAgentColor } = useContext( + SimulariumSimulationContext, + ); + const { section, progressionElement } = useContext(SimulariumUiContext); const [width, setWidth] = useState(0); const MARGINS = 64.2; // on super small screens this can result in a negative number const widthMinusMargins = Math.max(width - MARGINS, 0); const [highlightState, setHighlightState] = useState( - HighlightState.Initial + HighlightState.Initial, ); if ( @@ -75,7 +75,7 @@ const Concentration: React.FC = ({ const getComponent = ( agent: AgentName, - currentConcentrationOfAgent: number + currentConcentrationOfAgent: number, ) => { if (adjustableAgent === agent && !isPlaying) { return ( @@ -175,7 +175,7 @@ const Concentration: React.FC = ({ > {getComponent( agent, - agentLiveConcentration + agentLiveConcentration, )} {MICRO}M @@ -183,7 +183,7 @@ const Concentration: React.FC = ({ ); - } + }, )} diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx index 4574b38..7076e14 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect, useMemo, useRef } from "react"; import { SliderSingleProps } from "antd"; import Slider from "../shared/Slider"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumAnalysisContext } from "../../simulation/context"; import styles from "./concentration-slider.module.css"; import classNames from "classnames"; @@ -20,7 +20,7 @@ const Mark: React.FC<{ disabledNumbers: number[]; onMouseUp: () => void; }> = ({ index, disabledNumbers, onMouseUp }) => { - const { recordedConcentrations } = useContext(SimulariumContext); + const { recordedConcentrations } = useContext(SimulariumAnalysisContext); const ref = useRef(null); useEffect(() => { diff --git a/src/components/concentration-display/LiveConcentrationDisplay.tsx b/src/components/concentration-display/LiveConcentrationDisplay.tsx index 9734a1a..40eac95 100644 --- a/src/components/concentration-display/LiveConcentrationDisplay.tsx +++ b/src/components/concentration-display/LiveConcentrationDisplay.tsx @@ -3,7 +3,7 @@ import React, { useContext } from "react"; import { AgentName } from "../../types"; import styles from "./live-concentration-display.module.css"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumSimulationContext } from "../../simulation/context"; interface LiveConcentrationDisplayProps { concentration: number; @@ -16,7 +16,9 @@ const LiveConcentrationDisplay: React.FC = ({ concentration, width, }) => { - const { maxConcentration, getAgentColor } = useContext(SimulariumContext); + const { maxConcentration, getAgentColor } = useContext( + SimulariumSimulationContext, + ); // the steps have a 2px gap, so we are adjusting the // size of the step based on the total number we want const steps = maxConcentration; diff --git a/src/components/main-layout/ContentPanelTimer.tsx b/src/components/main-layout/ContentPanelTimer.tsx index 81f7cbf..d93c0b9 100644 --- a/src/components/main-layout/ContentPanelTimer.tsx +++ b/src/components/main-layout/ContentPanelTimer.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from "react"; import classNames from "classnames"; import { isEqual } from "lodash"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumUiContext } from "../../simulation/context"; import { PageContent, Module } from "../../types"; import ContentPanel from "./ContentPanel"; @@ -29,7 +29,7 @@ const ContentPanelTimer: React.FC = ({ const previousContentRef = useRef(pageContent); const contentJustChanged = !isEqual( previousContentRef.current.content, - pageContent.content + pageContent.content, ); useEffect(() => { @@ -44,7 +44,7 @@ const ContentPanelTimer: React.FC = ({ // must be the same as the css transition time const FADE_TIME = 150; const updateRenderState = ( - currentRenderState: RenderState + currentRenderState: RenderState, ): NodeJS.Timeout | null => { previousContentRef.current = pageContent; setRenderState(currentRenderState); @@ -52,7 +52,7 @@ const ContentPanelTimer: React.FC = ({ if (currentRenderState <= RenderState.FullyVisible) { return setTimeout( () => updateRenderState(currentRenderState), - FADE_TIME + FADE_TIME, ); } else { return null; @@ -60,7 +60,7 @@ const ContentPanelTimer: React.FC = ({ }; const timer = setTimeout( () => updateRenderState(RenderState.NoRender), - FADE_TIME + FADE_TIME, ); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -72,7 +72,7 @@ const ContentPanelTimer: React.FC = ({ ? previousContentRef.current : pageContent; - const { page } = useContext(SimulariumContext); + const { page } = useContext(SimulariumUiContext); const pageNumber = contentJustChanged ? page - 1 : page; const containerClassNames = classNames([ styles.contentPanelWrapper, diff --git a/src/components/main-layout/NavPanel.tsx b/src/components/main-layout/NavPanel.tsx index d11cb7e..78901f9 100644 --- a/src/components/main-layout/NavPanel.tsx +++ b/src/components/main-layout/NavPanel.tsx @@ -2,7 +2,7 @@ import React, { useContext } from "react"; import { Flex } from "antd"; import PageIndicator from "../PageIndicator"; import Dropdown from "../shared/Dropdown"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumUiContext } from "../../simulation/context"; import LinkOut from "../icons/LinkOut"; interface NavPanelProps { @@ -12,7 +12,7 @@ interface NavPanelProps { } const NavPanel: React.FC = ({ title, page, total }) => { - const { setPage } = useContext(SimulariumContext); + const { setPage } = useContext(SimulariumUiContext); const helpMenuItems = [ { key: "1", diff --git a/src/components/main-layout/RightPanel.tsx b/src/components/main-layout/RightPanel.tsx index b061b55..2630727 100644 --- a/src/components/main-layout/RightPanel.tsx +++ b/src/components/main-layout/RightPanel.tsx @@ -9,7 +9,7 @@ import { ProductOverTimeTrace } from "../plots/types"; import styles from "./layout.module.css"; import { AB, AC } from "../agent-symbols"; import ResizeContainer from "../shared/ResizeContainer"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumSimulationContext } from "../../simulation/context"; import HelpPopup from "../HelpPopup"; import InfoText from "../shared/InfoText"; import { ProductName, UiElement } from "../../types"; @@ -40,7 +40,7 @@ const RightPanel: React.FC = ({ currentAdjustableAgentConcentration, showHelpPanel, }) => { - const { productName } = useContext(SimulariumContext); + const { productName } = useContext(SimulariumSimulationContext); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); let data = productOverTimeTraces; diff --git a/src/components/plots/EquilibriumPlot.tsx b/src/components/plots/EquilibriumPlot.tsx index 42c12a8..83995ee 100644 --- a/src/components/plots/EquilibriumPlot.tsx +++ b/src/components/plots/EquilibriumPlot.tsx @@ -8,7 +8,10 @@ import { CONFIG, GRAY_COLOR, } from "./constants"; -import { SimulariumContext } from "../../simulation/context"; +import { + SimulariumSimulationContext, + SimulariumUiContext, +} from "../../simulation/context"; import { AGENT_A_COLOR, AGENT_AB_COLOR } from "../../constants/colors"; import { MICRO } from "../../constants"; @@ -38,8 +41,8 @@ const EquilibriumPlot: React.FC = ({ productName, getAgentColor, adjustableAgentName, - module, - } = useContext(SimulariumContext); + } = useContext(SimulariumSimulationContext); + const { module } = useContext(SimulariumUiContext); const xMax = Math.max(...x); const xAxisMax = Math.max(kd * 2, xMax * 1.1); diff --git a/src/components/plots/EventsOverTimePlot.tsx b/src/components/plots/EventsOverTimePlot.tsx index da57e5c..f2c760f 100644 --- a/src/components/plots/EventsOverTimePlot.tsx +++ b/src/components/plots/EventsOverTimePlot.tsx @@ -8,7 +8,10 @@ import { BASE_PLOT_LAYOUT, CONFIG, } from "./constants"; -import { SimulariumContext } from "../../simulation/context"; +import { + SimulariumSimulationContext, + SimulariumUiContext, +} from "../../simulation/context"; import { A, B, AB, C, AC } from "../agent-symbols"; import { MICRO } from "../../constants"; @@ -27,13 +30,14 @@ const EventsOverTimePlot: React.FC = ({ bindingEventsOverTime, unbindingEventsOverTime, }) => { - const { timeFactor, module } = useContext(SimulariumContext); + const { timeFactor } = useContext(SimulariumSimulationContext); + const { module } = useContext(SimulariumUiContext); const [width, setWidth] = useState(0); // the two arrays will always be the same length // so this time calculation only needs to happen once const time = bindingEventsOverTime.map( - (_, i) => (i * 10 * timeFactor) / 1000 + (_, i) => (i * 10 * timeFactor) / 1000, ); const max = useRef(0); diff --git a/src/components/plots/ProductConcentrationPlot.tsx b/src/components/plots/ProductConcentrationPlot.tsx index 7484d0f..abbe755 100644 --- a/src/components/plots/ProductConcentrationPlot.tsx +++ b/src/components/plots/ProductConcentrationPlot.tsx @@ -11,7 +11,7 @@ import { PLOT_COLORS, } from "./constants"; import { ProductOverTimeTrace } from "./types"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumSimulationContext } from "../../simulation/context"; import { MICRO } from "../../constants"; import { getColorIndex, indexToTime } from "../../utils"; @@ -41,7 +41,7 @@ const ProductConcentrationPlot: React.FC = ({ productName, adjustableAgentName, getAgentColor, - } = useContext(SimulariumContext); + } = useContext(SimulariumSimulationContext); const hasData = useRef(false); if (data.length === 0) { hasData.current = false; @@ -70,7 +70,7 @@ const ProductConcentrationPlot: React.FC = ({ } const timeArray = productConcentrations.map((_, i) => - indexToTime(i, timeFactor, timeUnit) + indexToTime(i, timeFactor, timeUnit), ); return { x: timeArray, diff --git a/src/components/quiz-questions/EquilibriumQuestion.tsx b/src/components/quiz-questions/EquilibriumQuestion.tsx index e4633e8..a0b5f97 100644 --- a/src/components/quiz-questions/EquilibriumQuestion.tsx +++ b/src/components/quiz-questions/EquilibriumQuestion.tsx @@ -4,11 +4,11 @@ import VisibilityControl from "../shared/VisibilityControl"; import { FormState } from "./types"; import RadioComponent from "../shared/Radio"; import { EQUILIBRIUM_QUIZ_ID } from "../../constants"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumUiContext } from "../../simulation/context"; import { Module } from "../../types"; const EquilibriumQuestion: React.FC = () => { - const { page, quizQuestion, module } = useContext(SimulariumContext); + const { page, quizQuestion, module } = useContext(SimulariumUiContext); const [selectedAnswer, setSelectedAnswer] = useState(""); const [formState, setFormState] = useState(FormState.Clear); const firstVisiblePage = useRef<{ page: number; module: Module }>({ diff --git a/src/components/quiz-questions/KdQuestion.tsx b/src/components/quiz-questions/KdQuestion.tsx index ed09c26..81c32c6 100644 --- a/src/components/quiz-questions/KdQuestion.tsx +++ b/src/components/quiz-questions/KdQuestion.tsx @@ -8,7 +8,7 @@ import InputNumber from "../shared/InputNumber"; import { FormState } from "./types"; import styles from "./popup.module.css"; import { MICRO } from "../../constants"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumUiContext } from "../../simulation/context"; interface KdQuestionProps { kd: number; @@ -19,7 +19,7 @@ const KdQuestion: React.FC = ({ kd, canAnswer }) => { const [selectedAnswer, setSelectedAnswer] = useState(null); const [formState, setFormState] = useState(FormState.Clear); - const { module, addCompletedModule } = useContext(SimulariumContext); + const { module, addCompletedModule } = useContext(SimulariumUiContext); useEffect(() => { setSelectedAnswer(null); diff --git a/src/components/shared/BackButton.tsx b/src/components/shared/BackButton.tsx index a14baa7..189536a 100644 --- a/src/components/shared/BackButton.tsx +++ b/src/components/shared/BackButton.tsx @@ -1,9 +1,9 @@ import { useContext } from "react"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumUiContext } from "../../simulation/context"; import { SecondaryButton } from "./ButtonLibrary"; const BackButton = () => { - const { page, setPage } = useContext(SimulariumContext); + const { page, setPage } = useContext(SimulariumUiContext); return ( setPage(page - 1)}> diff --git a/src/components/shared/NextButton.tsx b/src/components/shared/NextButton.tsx index 225c0b3..73f067c 100644 --- a/src/components/shared/NextButton.tsx +++ b/src/components/shared/NextButton.tsx @@ -1,5 +1,5 @@ import { useContext } from "react"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumUiContext } from "../../simulation/context"; import { PrimaryButton } from "./ButtonLibrary"; import useModule from "../../hooks/useModule"; @@ -8,7 +8,8 @@ interface NextButtonProps { } const NextButton = ({ text }: NextButtonProps) => { - const { page, setPage, module, setModule } = useContext(SimulariumContext); + const { page, setPage, module, setModule } = + useContext(SimulariumUiContext); const { totalPages } = useModule(module); if (page + 1 > totalPages) { diff --git a/src/components/shared/ProgressionControl.tsx b/src/components/shared/ProgressionControl.tsx index 6216a3d..0275056 100644 --- a/src/components/shared/ProgressionControl.tsx +++ b/src/components/shared/ProgressionControl.tsx @@ -1,5 +1,5 @@ import React, { useContext } from "react"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumUiContext } from "../../simulation/context"; import { BaseHandler, ProgressionControlEvent } from "../../types"; import styles from "./progression-control.module.css"; @@ -24,7 +24,8 @@ const ProgressionControl: React.FC = ({ children, elementId, }) => { - const { page, setPage, progressionElement } = useContext(SimulariumContext); + const { page, setPage, progressionElement } = + useContext(SimulariumUiContext); const shouldProgress = progressionElement === elementId; const progress = () => { if (shouldProgress) { @@ -37,7 +38,7 @@ const ProgressionControl: React.FC = ({ const mergeHandlers = (baseHandler: BaseHandler) => { return ( event: ProgressionControlEvent, - optionalValue?: string | number | string[] | number[] + optionalValue?: string | number | string[] | number[], ) => { const returnValue = baseHandler(event, optionalValue); // generally, all handlers are going to return undefined diff --git a/src/components/shared/VisibilityControl.tsx b/src/components/shared/VisibilityControl.tsx index 2eaf598..81a6d50 100644 --- a/src/components/shared/VisibilityControl.tsx +++ b/src/components/shared/VisibilityControl.tsx @@ -1,5 +1,5 @@ import React, { useContext } from "react"; -import { SimulariumContext } from "../../simulation/context"; +import { SimulariumUiContext } from "../../simulation/context"; import { Module, Section } from "../../types"; interface VisibilityControlProps { @@ -21,7 +21,7 @@ const VisibilityControl: React.FC = ({ notInIntroduction, startPage, }) => { - const { page, section, module } = useContext(SimulariumContext); + const { page, section, module } = useContext(SimulariumUiContext); if (conditionalRender === false) { return null; } diff --git a/src/simulation/context.tsx b/src/simulation/context.tsx index 82cb55d..f4c18b5 100644 --- a/src/simulation/context.tsx +++ b/src/simulation/context.tsx @@ -12,7 +12,22 @@ import { } from "../constants"; import { AgentName, Module, ProductName, Section, ViewType } from "../types"; -interface SimulariumContextType { +export interface SimulariumUiContextType { + addCompletedModule: (value: Module) => void; + completedModules: Set; + module: Module; + page: number; + progressionElement: ProgressionElement | ""; + quizQuestion: string; + resetAllState: () => void; + section: Section; + setModule: (value: Module) => void; + setPage: (value: number) => void; + setViewportType: () => void; + viewportType: ViewType; +} + +export interface SimulariumSimulationContextType { adjustableAgentName: AgentName; currentProductionConcentration: number; fixedAgentStartingConcentration: number; @@ -23,29 +38,37 @@ interface SimulariumContextType { handleTrajectoryChange: (value: TrajectoryFileInfo) => void; isPlaying: boolean; maxConcentration: number; - module: Module; - page: number; productName: ProductName; - progressionElement: ProgressionElement | ""; - quizQuestion: string; - recordedConcentrations: number[]; - section: Section; setIsPlaying: (value: boolean) => void; - setModule: (value: Module) => void; - setPage: (value: number) => void; setViewportSize: (value: { width: number; height: number }) => void; - setViewportType: () => void; simulariumController: SimulariumController | null; timeFactor: number; timeUnit: string; trajectoryName: string; viewportSize: { width: number; height: number }; - viewportType: ViewType; - addCompletedModule: (value: Module) => void; - completedModules: Set; } -export const SimulariumContext = createContext({ +export interface SimulariumAnalysisContextType { + recordedConcentrations: number[]; + resetAnalysisState: () => void; +} + +export const SimulariumUiContext = createContext({ + addCompletedModule: () => {}, + completedModules: new Set(), + module: Module.A_B_AB, + page: 0, + progressionElement: "", + quizQuestion: "", + resetAllState: () => {}, + section: Section.Introduction, + setModule: () => {}, + setPage: () => {}, + setViewportType: () => {}, + viewportType: ViewType.Lab, +} as SimulariumUiContextType); + +export const SimulariumSimulationContext = createContext({ adjustableAgentName: AgentName.B, currentProductionConcentration: 0, fixedAgentStartingConcentration: 0, @@ -56,24 +79,17 @@ export const SimulariumContext = createContext({ handleTrajectoryChange: () => {}, isPlaying: false, maxConcentration: 10, - module: Module.A_B_AB, - page: 0, productName: ProductName.AB, - progressionElement: "", - quizQuestion: "", - recordedConcentrations: [], - section: Section.Introduction, setIsPlaying: () => {}, - setModule: () => {}, - setPage: () => {}, setViewportSize: () => {}, - setViewportType: () => {}, simulariumController: null, timeFactor: 30, timeUnit: NANO, trajectoryName: LIVE_SIMULATION_NAME, viewportSize: DEFAULT_VIEWPORT_SIZE, - viewportType: ViewType.Lab, - addCompletedModule: () => {}, - completedModules: new Set(), -} as SimulariumContextType); +} as SimulariumSimulationContextType); + +export const SimulariumAnalysisContext = createContext({ + recordedConcentrations: [], + resetAnalysisState: () => {}, +} as SimulariumAnalysisContextType); From 4bf2562b67ca1a291cd1d40dba987ba20b6ee43e Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 27 Mar 2026 13:25:19 -0700 Subject: [PATCH 04/23] new context --- CONTEXT_ORGANIZATION.md | 80 +++++++++++++++++++ src/components/AdminUi.tsx | 7 +- src/components/LabView.tsx | 12 +-- src/components/MixButton.tsx | 6 +- src/components/PageIndicator.tsx | 5 +- src/components/PlayButton.tsx | 6 +- src/components/ScaleBar.tsx | 6 +- src/components/StartExperiment.tsx | 6 +- src/components/ViewSwitch.tsx | 16 ++-- src/components/Viewer.tsx | 19 ++--- .../concentration-display/Concentration.tsx | 15 ++-- .../ConcentrationSlider.tsx | 6 +- .../LiveConcentrationDisplay.tsx | 8 +- .../main-layout/ContentPanelTimer.tsx | 6 +- src/components/main-layout/LeftPanel.tsx | 9 ++- src/components/main-layout/NavPanel.tsx | 6 +- src/components/main-layout/RightPanel.tsx | 6 +- src/components/plots/EquilibriumPlot.tsx | 12 +-- src/components/plots/EventsOverTimePlot.tsx | 12 +-- .../plots/ProductConcentrationPlot.tsx | 6 +- .../quiz-questions/EquilibriumQuestion.tsx | 6 +- src/components/quiz-questions/KdQuestion.tsx | 6 +- src/components/shared/BackButton.tsx | 5 +- src/components/shared/NextButton.tsx | 6 +- src/components/shared/ProgressionControl.tsx | 7 +- src/components/shared/ResetButton.tsx | 40 ++++++++++ src/components/shared/VisibilityControl.tsx | 6 +- src/hooks/useSimulationContext.ts | 30 +++++++ 28 files changed, 247 insertions(+), 108 deletions(-) create mode 100644 CONTEXT_ORGANIZATION.md create mode 100644 src/components/shared/ResetButton.tsx create mode 100644 src/hooks/useSimulationContext.ts diff --git a/CONTEXT_ORGANIZATION.md b/CONTEXT_ORGANIZATION.md new file mode 100644 index 0000000..96bf4b4 --- /dev/null +++ b/CONTEXT_ORGANIZATION.md @@ -0,0 +1,80 @@ +# Context Organization + +The app context has been organized into three separate contexts for better separation of concerns and easier state management. + +## Contexts + +### SimulariumUiContext +Contains UI-related state: +- `page`, `setPage` - Current page number +- `module`, `setModule` - Current module +- `progressionElement` - Current progression element for hints +- `quizQuestion` - Current quiz question +- `section` - Current section +- `viewportType`, `setViewportType` - Lab vs Simulation view +- `completedModules`, `addCompletedModule` - Track module completion +- `resetAllState()` - Reset all application state + +### SimulariumSimulationContext +Contains simulation-related state: +- `isPlaying`, `setIsPlaying` - Playback state +- `simulariumController` - Viewer controller +- `trajectoryName` - Current trajectory +- `timeFactor`, `timeUnit` - Time settings +- `viewportSize`, `setViewportSize` - Viewer dimensions +- `productName`, `adjustableAgentName` - Agent info +- `currentProductionConcentration`, `fixedAgentStartingConcentration`, `maxConcentration` - Concentration values +- `getAgentColor()` - Color mapping +- `handleMixAgents()`, `handleStartExperiment()`, `handleTimeChange()`, `handleTrajectoryChange()` - Handlers + +### SimulariumAnalysisContext +Contains analysis and recorded data: +- `recordedConcentrations` - User-recorded concentration values +- `resetAnalysisState()` - Clear recorded data without resetting UI + +## Usage + +### Using the Hooks + +Import the hooks instead of using `useContext` directly: + +```tsx +import { + useSimulariumUi, + useSimulariumSimulation, + useSimulariumAnalysis, +} from "../hooks/useSimulationContext"; + +const MyComponent = () => { + const { page, setPage } = useSimulariumUi(); + const { isPlaying, setIsPlaying } = useSimulariumSimulation(); + const { recordedConcentrations } = useSimulariumAnalysis(); + + // Use the values... +}; +``` + +### Using the Reset Button + +A `ResetButton` component is available for resetting state: + +```tsx +import ResetButton from "../shared/ResetButton"; + +// Reset all state (UI + simulation + analysis) + + +// Reset only analysis data + + +// Custom label +Start Over +``` + +## Benefits + +1. **Clearer separation of concerns** - UI, simulation, and analysis state are separate +2. **Easier to reset** - Individual contexts can be reset without affecting others +3. **Better performance** - Components only re-render when their specific context changes +4. **Type safety** - Each context has its own strongly-typed interface +5. **Cleaner imports** - Use hooks instead of context + useContext everywhere diff --git a/src/components/AdminUi.tsx b/src/components/AdminUi.tsx index 7885257..9d97233 100644 --- a/src/components/AdminUi.tsx +++ b/src/components/AdminUi.tsx @@ -1,8 +1,8 @@ -import React, { useContext, useEffect } from "react"; +import React, { useEffect } from "react"; import Slider from "./shared/Slider"; import { BG_DARK, LIGHT_GREY } from "../constants/colors"; -import { SimulariumUiContext } from "../simulation/context"; +import { useSimulariumUi } from "../hooks/useSimulationContext"; import { InputNumber, SliderSingleProps } from "antd"; import { zStacking } from "../constants/z-stacking"; import { Module } from "../types"; @@ -18,8 +18,7 @@ const AdminUI: React.FC = ({ setTimeFactor, totalPages, }) => { - const { page, setPage, module, setModule } = - useContext(SimulariumUiContext); + const { page, setPage, module, setModule } = useSimulariumUi(); const [visible, setVisible] = React.useState(false); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { diff --git a/src/components/LabView.tsx b/src/components/LabView.tsx index b044c49..70a1fbe 100644 --- a/src/components/LabView.tsx +++ b/src/components/LabView.tsx @@ -1,9 +1,9 @@ import Rainbow from "rainbowvis.js"; -import { useContext, useMemo } from "react"; +import { useMemo } from "react"; import { - SimulariumSimulationContext, - SimulariumUiContext, -} from "../simulation/context"; + useSimulariumSimulation, + useSimulariumUi, +} from "../hooks/useSimulationContext"; import Cuvette from "./icons/Cuvette"; import styles from "./labview.module.css"; import classNames from "classnames"; @@ -17,8 +17,8 @@ const LabView: React.FC = () => { maxConcentration, getAgentColor, productName, - } = useContext(SimulariumSimulationContext); - const { page, module } = useContext(SimulariumUiContext); + } = useSimulariumSimulation(); + const { page, module } = useSimulariumUi(); const color = getAgentColor(productName); const colorGradient = useMemo(() => { const rainbow = new Rainbow(); diff --git a/src/components/MixButton.tsx b/src/components/MixButton.tsx index f683202..f7cb79a 100644 --- a/src/components/MixButton.tsx +++ b/src/components/MixButton.tsx @@ -1,13 +1,13 @@ -import React, { useContext } from "react"; +import React from "react"; -import { SimulariumSimulationContext } from "../simulation/context"; +import { useSimulariumSimulation } from "../hooks/useSimulationContext"; import { TertiaryButton } from "./shared/ButtonLibrary"; import { MIX_AGENTS_ID } from "../constants"; import ProgressionControl from "./shared/ProgressionControl"; import style from "./start-experiment.module.css"; const MixButton: React.FC = () => { - const { handleMixAgents } = useContext(SimulariumSimulationContext); + const { handleMixAgents } = useSimulariumSimulation(); return ( diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx index 118cb28..617cfd1 100644 --- a/src/components/PageIndicator.tsx +++ b/src/components/PageIndicator.tsx @@ -5,7 +5,7 @@ import classNames from "classnames"; import { moduleNames } from "../content"; import styles from "./page-indicator.module.css"; -import { SimulariumUiContext } from "../simulation/context"; +import { useSimulariumUi } from "../hooks/useSimulationContext"; interface PageIndicatorProps { title: string; @@ -18,8 +18,7 @@ const PageIndicator: React.FC = ({ page, total, }) => { - const { module, setModule, completedModules } = - React.useContext(SimulariumUiContext); + const { module, setModule, completedModules } = useSimulariumUi(); const indexOfActiveModule: number = useMemo(() => { let toReturn = -1; map(moduleNames, (name, index) => { diff --git a/src/components/PlayButton.tsx b/src/components/PlayButton.tsx index 28bc009..1aa5afc 100644 --- a/src/components/PlayButton.tsx +++ b/src/components/PlayButton.tsx @@ -1,7 +1,7 @@ -import React, { useContext } from "react"; +import React from "react"; import { CaretRightOutlined, PauseOutlined } from "@ant-design/icons"; -import { SimulariumSimulationContext } from "../simulation/context"; +import { useSimulariumSimulation } from "../hooks/useSimulationContext"; import ProgressionControl from "./shared/ProgressionControl"; import VisibilityControl from "./shared/VisibilityControl"; import { OverlayButton } from "./shared/ButtonLibrary"; @@ -9,7 +9,7 @@ import { Module } from "../types"; import { PLAY_BUTTON_ID } from "../constants"; const PlayButton: React.FC = () => { - const { isPlaying, setIsPlaying } = useContext(SimulariumSimulationContext); + const { isPlaying, setIsPlaying } = useSimulariumSimulation(); const handleClick = () => { setIsPlaying(!isPlaying); diff --git a/src/components/ScaleBar.tsx b/src/components/ScaleBar.tsx index 1bc3fa5..e34fc33 100644 --- a/src/components/ScaleBar.tsx +++ b/src/components/ScaleBar.tsx @@ -1,15 +1,15 @@ -import React, { useContext } from "react"; +import React from "react"; import styles from "./scalebar.module.css"; import { MICRO } from "../constants"; -import { SimulariumSimulationContext } from "../simulation/context"; +import { useSimulariumSimulation } from "../hooks/useSimulationContext"; interface ScaleBarProps { productColor: string; } const ScaleBar: React.FC = ({ productColor }) => { - const { maxConcentration } = useContext(SimulariumSimulationContext); + const { maxConcentration } = useSimulariumSimulation(); const labelArray = []; const interval = maxConcentration / 5; for (let i = maxConcentration; i >= 0; i = i - interval) { diff --git a/src/components/StartExperiment.tsx b/src/components/StartExperiment.tsx index f583731..64931bd 100644 --- a/src/components/StartExperiment.tsx +++ b/src/components/StartExperiment.tsx @@ -1,13 +1,13 @@ -import React, { useContext } from "react"; +import React from "react"; import classNames from "classnames"; -import { SimulariumSimulationContext } from "../simulation/context"; +import { useSimulariumSimulation } from "../hooks/useSimulationContext"; import { TertiaryButton } from "./shared/ButtonLibrary"; import style from "./start-experiment.module.css"; const StartExperiment: React.FC = () => { - const { handleStartExperiment } = useContext(SimulariumSimulationContext); + const { handleStartExperiment } = useSimulariumSimulation(); const [initial, setIsInitial] = React.useState(true); const handleClick = () => { if (initial) { diff --git a/src/components/ViewSwitch.tsx b/src/components/ViewSwitch.tsx index c6eec86..71ff600 100644 --- a/src/components/ViewSwitch.tsx +++ b/src/components/ViewSwitch.tsx @@ -1,10 +1,10 @@ -import React, { useContext } from "react"; +import React from "react"; import Viewer from "./Viewer"; import { - SimulariumSimulationContext, - SimulariumUiContext, -} from "../simulation/context"; + useSimulariumSimulation, + useSimulariumUi, +} from "../hooks/useSimulationContext"; import ProgressionControl from "./shared/ProgressionControl"; import PlayButton from "./PlayButton"; import { OverlayButton } from "./shared/ButtonLibrary"; @@ -17,11 +17,9 @@ import { FIRST_PAGE } from "../content"; import { VIEW_SWITCH_ID } from "../constants"; const ViewSwitch: React.FC = () => { - const { viewportType, setViewportType, page, module } = - useContext(SimulariumUiContext); - const { isPlaying, setIsPlaying, handleTimeChange } = useContext( - SimulariumSimulationContext, - ); + const { viewportType, setViewportType, page, module } = useSimulariumUi(); + const { isPlaying, setIsPlaying, handleTimeChange } = + useSimulariumSimulation(); const isFirstPageOfFirstModule = page === FIRST_PAGE[module] + 1 && module === Module.A_B_AB; diff --git a/src/components/Viewer.tsx b/src/components/Viewer.tsx index 2260dbe..9beab24 100644 --- a/src/components/Viewer.tsx +++ b/src/components/Viewer.tsx @@ -1,11 +1,4 @@ -import { - ReactNode, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from "react"; +import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import classNames from "classnames"; import SimulariumViewer, { RenderStyle, @@ -14,9 +7,9 @@ import SimulariumViewer, { import "@aics/simularium-viewer/style/style.css"; import { - SimulariumSimulationContext, - SimulariumUiContext, -} from "../simulation/context"; + useSimulariumSimulation, + useSimulariumUi, +} from "../hooks/useSimulationContext"; import styles from "./viewer.module.css"; import useWindowResize from "../hooks/useWindowResize"; import { LIVE_SIMULATION_NAME } from "../constants"; @@ -46,8 +39,8 @@ export default function Viewer({ handleTimeChange }: ViewerProps): ReactNode { simulariumController, handleTrajectoryChange, trajectoryName, - } = useContext(SimulariumSimulationContext); - const { page } = useContext(SimulariumUiContext); + } = useSimulariumSimulation(); + const { page } = useSimulariumUi(); const setViewportToContainerSize = useCallback(() => { if (container.current) { diff --git a/src/components/concentration-display/Concentration.tsx b/src/components/concentration-display/Concentration.tsx index 2d8d2dc..51d8491 100644 --- a/src/components/concentration-display/Concentration.tsx +++ b/src/components/concentration-display/Concentration.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; import { map } from "lodash"; import { Flex } from "antd"; import classNames from "classnames"; @@ -11,9 +11,9 @@ import { UiElement, } from "../../types"; import { - SimulariumSimulationContext, - SimulariumUiContext, -} from "../../simulation/context"; + useSimulariumSimulation, + useSimulariumUi, +} from "../../hooks/useSimulationContext"; import LiveConcentrationDisplay from "./LiveConcentrationDisplay"; import ConcentrationSlider from "./ConcentrationSlider"; import { MICRO, CHANGE_CONCENTRATION_ID } from "../../constants"; @@ -45,10 +45,9 @@ const Concentration: React.FC = ({ liveConcentration, onChangeComplete, }) => { - const { isPlaying, maxConcentration, getAgentColor } = useContext( - SimulariumSimulationContext, - ); - const { section, progressionElement } = useContext(SimulariumUiContext); + const { isPlaying, maxConcentration, getAgentColor } = + useSimulariumSimulation(); + const { section, progressionElement } = useSimulariumUi(); const [width, setWidth] = useState(0); const MARGINS = 64.2; diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx index 7076e14..f195e0f 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -1,8 +1,8 @@ -import React, { useContext, useEffect, useMemo, useRef } from "react"; +import React, { useEffect, useMemo, useRef } from "react"; import { SliderSingleProps } from "antd"; import Slider from "../shared/Slider"; -import { SimulariumAnalysisContext } from "../../simulation/context"; +import { useSimulariumAnalysis } from "../../hooks/useSimulationContext"; import styles from "./concentration-slider.module.css"; import classNames from "classnames"; @@ -20,7 +20,7 @@ const Mark: React.FC<{ disabledNumbers: number[]; onMouseUp: () => void; }> = ({ index, disabledNumbers, onMouseUp }) => { - const { recordedConcentrations } = useContext(SimulariumAnalysisContext); + const { recordedConcentrations } = useSimulariumAnalysis(); const ref = useRef(null); useEffect(() => { diff --git a/src/components/concentration-display/LiveConcentrationDisplay.tsx b/src/components/concentration-display/LiveConcentrationDisplay.tsx index 40eac95..1800976 100644 --- a/src/components/concentration-display/LiveConcentrationDisplay.tsx +++ b/src/components/concentration-display/LiveConcentrationDisplay.tsx @@ -1,9 +1,9 @@ import { Flex, Progress } from "antd"; -import React, { useContext } from "react"; +import React from "react"; import { AgentName } from "../../types"; import styles from "./live-concentration-display.module.css"; -import { SimulariumSimulationContext } from "../../simulation/context"; +import { useSimulariumSimulation } from "../../hooks/useSimulationContext"; interface LiveConcentrationDisplayProps { concentration: number; @@ -16,9 +16,7 @@ const LiveConcentrationDisplay: React.FC = ({ concentration, width, }) => { - const { maxConcentration, getAgentColor } = useContext( - SimulariumSimulationContext, - ); + const { maxConcentration, getAgentColor } = useSimulariumSimulation(); // the steps have a 2px gap, so we are adjusting the // size of the step based on the total number we want const steps = maxConcentration; diff --git a/src/components/main-layout/ContentPanelTimer.tsx b/src/components/main-layout/ContentPanelTimer.tsx index d93c0b9..8eedf24 100644 --- a/src/components/main-layout/ContentPanelTimer.tsx +++ b/src/components/main-layout/ContentPanelTimer.tsx @@ -1,8 +1,8 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import classNames from "classnames"; import { isEqual } from "lodash"; -import { SimulariumUiContext } from "../../simulation/context"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; import { PageContent, Module } from "../../types"; import ContentPanel from "./ContentPanel"; @@ -72,7 +72,7 @@ const ContentPanelTimer: React.FC = ({ ? previousContentRef.current : pageContent; - const { page } = useContext(SimulariumUiContext); + const { page } = useSimulariumUi(); const pageNumber = contentJustChanged ? page - 1 : page; const containerClassNames = classNames([ styles.contentPanelWrapper, diff --git a/src/components/main-layout/LeftPanel.tsx b/src/components/main-layout/LeftPanel.tsx index 851f368..5314a9f 100644 --- a/src/components/main-layout/LeftPanel.tsx +++ b/src/components/main-layout/LeftPanel.tsx @@ -6,6 +6,7 @@ import { InputConcentration, Module, } from "../../types"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; import VisibilityControl from "../shared/VisibilityControl"; import EventsOverTimePlot from "../plots/EventsOverTimePlot"; import Concentration from "../concentration-display/Concentration"; @@ -29,6 +30,8 @@ const LeftPanel: React.FC = ({ unbindingEventsOverTime, adjustableAgent, }) => { + const { module } = useSimulariumUi(); + const concentrationExcludedPages = { [Module.A_B_AB]: [0, 1], [Module.A_C_AC]: [], @@ -39,6 +42,10 @@ const LeftPanel: React.FC = ({ [Module.A_B_AB]: [0, 1, 2], [Module.A_C_AC]: [], }; + + // Don't show events over time plot in the competitive binding module + const showEventsOverTime = module !== Module.A_B_D_AB; + return ( <> @@ -52,7 +59,7 @@ const LeftPanel: React.FC = ({ = ({ title, page, total }) => { - const { setPage } = useContext(SimulariumUiContext); + const { setPage } = useSimulariumUi(); const helpMenuItems = [ { key: "1", diff --git a/src/components/main-layout/RightPanel.tsx b/src/components/main-layout/RightPanel.tsx index 2630727..aaed196 100644 --- a/src/components/main-layout/RightPanel.tsx +++ b/src/components/main-layout/RightPanel.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useContext, useState } from "react"; +import React, { ReactNode, useState } from "react"; import VisibilityControl from "../shared/VisibilityControl"; import ProductConcentrationPlot from "../plots/ProductConcentrationPlot"; @@ -9,7 +9,7 @@ import { ProductOverTimeTrace } from "../plots/types"; import styles from "./layout.module.css"; import { AB, AC } from "../agent-symbols"; import ResizeContainer from "../shared/ResizeContainer"; -import { SimulariumSimulationContext } from "../../simulation/context"; +import { useSimulariumSimulation } from "../../hooks/useSimulationContext"; import HelpPopup from "../HelpPopup"; import InfoText from "../shared/InfoText"; import { ProductName, UiElement } from "../../types"; @@ -40,7 +40,7 @@ const RightPanel: React.FC = ({ currentAdjustableAgentConcentration, showHelpPanel, }) => { - const { productName } = useContext(SimulariumSimulationContext); + const { productName } = useSimulariumSimulation(); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); let data = productOverTimeTraces; diff --git a/src/components/plots/EquilibriumPlot.tsx b/src/components/plots/EquilibriumPlot.tsx index 83995ee..bc3cfb9 100644 --- a/src/components/plots/EquilibriumPlot.tsx +++ b/src/components/plots/EquilibriumPlot.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useMemo } from "react"; +import React, { useMemo } from "react"; import regression, { DataPoint } from "regression"; import Plot from "react-plotly.js"; @@ -9,9 +9,9 @@ import { GRAY_COLOR, } from "./constants"; import { - SimulariumSimulationContext, - SimulariumUiContext, -} from "../../simulation/context"; + useSimulariumSimulation, + useSimulariumUi, +} from "../../hooks/useSimulationContext"; import { AGENT_A_COLOR, AGENT_AB_COLOR } from "../../constants/colors"; import { MICRO } from "../../constants"; @@ -41,8 +41,8 @@ const EquilibriumPlot: React.FC = ({ productName, getAgentColor, adjustableAgentName, - } = useContext(SimulariumSimulationContext); - const { module } = useContext(SimulariumUiContext); + } = useSimulariumSimulation(); + const { module } = useSimulariumUi(); const xMax = Math.max(...x); const xAxisMax = Math.max(kd * 2, xMax * 1.1); diff --git a/src/components/plots/EventsOverTimePlot.tsx b/src/components/plots/EventsOverTimePlot.tsx index f2c760f..558bb31 100644 --- a/src/components/plots/EventsOverTimePlot.tsx +++ b/src/components/plots/EventsOverTimePlot.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useRef, useState } from "react"; +import React, { useRef, useState } from "react"; import { Flex } from "antd"; import Plot from "react-plotly.js"; @@ -9,9 +9,9 @@ import { CONFIG, } from "./constants"; import { - SimulariumSimulationContext, - SimulariumUiContext, -} from "../../simulation/context"; + useSimulariumSimulation, + useSimulariumUi, +} from "../../hooks/useSimulationContext"; import { A, B, AB, C, AC } from "../agent-symbols"; import { MICRO } from "../../constants"; @@ -30,8 +30,8 @@ const EventsOverTimePlot: React.FC = ({ bindingEventsOverTime, unbindingEventsOverTime, }) => { - const { timeFactor } = useContext(SimulariumSimulationContext); - const { module } = useContext(SimulariumUiContext); + const { timeFactor } = useSimulariumSimulation(); + const { module } = useSimulariumUi(); const [width, setWidth] = useState(0); // the two arrays will always be the same length diff --git a/src/components/plots/ProductConcentrationPlot.tsx b/src/components/plots/ProductConcentrationPlot.tsx index abbe755..9f42380 100644 --- a/src/components/plots/ProductConcentrationPlot.tsx +++ b/src/components/plots/ProductConcentrationPlot.tsx @@ -1,5 +1,5 @@ import { PlotData } from "plotly.js"; -import React, { useContext, useRef } from "react"; +import React, { useRef } from "react"; import Plot from "react-plotly.js"; import { @@ -11,7 +11,7 @@ import { PLOT_COLORS, } from "./constants"; import { ProductOverTimeTrace } from "./types"; -import { SimulariumSimulationContext } from "../../simulation/context"; +import { useSimulariumSimulation } from "../../hooks/useSimulationContext"; import { MICRO } from "../../constants"; import { getColorIndex, indexToTime } from "../../utils"; @@ -41,7 +41,7 @@ const ProductConcentrationPlot: React.FC = ({ productName, adjustableAgentName, getAgentColor, - } = useContext(SimulariumSimulationContext); + } = useSimulariumSimulation(); const hasData = useRef(false); if (data.length === 0) { hasData.current = false; diff --git a/src/components/quiz-questions/EquilibriumQuestion.tsx b/src/components/quiz-questions/EquilibriumQuestion.tsx index a0b5f97..7d3f8bb 100644 --- a/src/components/quiz-questions/EquilibriumQuestion.tsx +++ b/src/components/quiz-questions/EquilibriumQuestion.tsx @@ -1,14 +1,14 @@ -import React, { useContext, useRef, useState } from "react"; +import React, { useRef, useState } from "react"; import QuizForm from "./QuizForm"; import VisibilityControl from "../shared/VisibilityControl"; import { FormState } from "./types"; import RadioComponent from "../shared/Radio"; import { EQUILIBRIUM_QUIZ_ID } from "../../constants"; -import { SimulariumUiContext } from "../../simulation/context"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; import { Module } from "../../types"; const EquilibriumQuestion: React.FC = () => { - const { page, quizQuestion, module } = useContext(SimulariumUiContext); + const { page, quizQuestion, module } = useSimulariumUi(); const [selectedAnswer, setSelectedAnswer] = useState(""); const [formState, setFormState] = useState(FormState.Clear); const firstVisiblePage = useRef<{ page: number; module: Module }>({ diff --git a/src/components/quiz-questions/KdQuestion.tsx b/src/components/quiz-questions/KdQuestion.tsx index 81c32c6..8f4fec1 100644 --- a/src/components/quiz-questions/KdQuestion.tsx +++ b/src/components/quiz-questions/KdQuestion.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { valueType } from "antd/es/statistic/utils"; import { Flex } from "antd"; @@ -8,7 +8,7 @@ import InputNumber from "../shared/InputNumber"; import { FormState } from "./types"; import styles from "./popup.module.css"; import { MICRO } from "../../constants"; -import { SimulariumUiContext } from "../../simulation/context"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; interface KdQuestionProps { kd: number; @@ -19,7 +19,7 @@ const KdQuestion: React.FC = ({ kd, canAnswer }) => { const [selectedAnswer, setSelectedAnswer] = useState(null); const [formState, setFormState] = useState(FormState.Clear); - const { module, addCompletedModule } = useContext(SimulariumUiContext); + const { module, addCompletedModule } = useSimulariumUi(); useEffect(() => { setSelectedAnswer(null); diff --git a/src/components/shared/BackButton.tsx b/src/components/shared/BackButton.tsx index 189536a..c05cf42 100644 --- a/src/components/shared/BackButton.tsx +++ b/src/components/shared/BackButton.tsx @@ -1,9 +1,8 @@ -import { useContext } from "react"; -import { SimulariumUiContext } from "../../simulation/context"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; import { SecondaryButton } from "./ButtonLibrary"; const BackButton = () => { - const { page, setPage } = useContext(SimulariumUiContext); + const { page, setPage } = useSimulariumUi(); return ( setPage(page - 1)}> diff --git a/src/components/shared/NextButton.tsx b/src/components/shared/NextButton.tsx index 73f067c..e542b75 100644 --- a/src/components/shared/NextButton.tsx +++ b/src/components/shared/NextButton.tsx @@ -1,5 +1,4 @@ -import { useContext } from "react"; -import { SimulariumUiContext } from "../../simulation/context"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; import { PrimaryButton } from "./ButtonLibrary"; import useModule from "../../hooks/useModule"; @@ -8,8 +7,7 @@ interface NextButtonProps { } const NextButton = ({ text }: NextButtonProps) => { - const { page, setPage, module, setModule } = - useContext(SimulariumUiContext); + const { page, setPage, module, setModule } = useSimulariumUi(); const { totalPages } = useModule(module); if (page + 1 > totalPages) { diff --git a/src/components/shared/ProgressionControl.tsx b/src/components/shared/ProgressionControl.tsx index 0275056..18b5c53 100644 --- a/src/components/shared/ProgressionControl.tsx +++ b/src/components/shared/ProgressionControl.tsx @@ -1,5 +1,5 @@ -import React, { useContext } from "react"; -import { SimulariumUiContext } from "../../simulation/context"; +import React from "react"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; import { BaseHandler, ProgressionControlEvent } from "../../types"; import styles from "./progression-control.module.css"; @@ -24,8 +24,7 @@ const ProgressionControl: React.FC = ({ children, elementId, }) => { - const { page, setPage, progressionElement } = - useContext(SimulariumUiContext); + const { page, setPage, progressionElement } = useSimulariumUi(); const shouldProgress = progressionElement === elementId; const progress = () => { if (shouldProgress) { diff --git a/src/components/shared/ResetButton.tsx b/src/components/shared/ResetButton.tsx new file mode 100644 index 0000000..cd1bef2 --- /dev/null +++ b/src/components/shared/ResetButton.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { + useSimulariumAnalysis, + useSimulariumUi, +} from "../../hooks/useSimulationContext"; +import { SecondaryButton } from "./ButtonLibrary"; + +interface ResetButtonProps { + /** If true, resets all state including UI. If false, only resets analysis data */ + resetAll?: boolean; + children?: React.ReactNode; +} + +/** + * Button that resets either all application state or just analysis state + * based on the resetAll prop + */ +const ResetButton: React.FC = ({ + resetAll = false, + children, +}) => { + const { resetAllState } = useSimulariumUi(); + const { resetAnalysisState } = useSimulariumAnalysis(); + + const handleClick = () => { + if (resetAll) { + resetAllState(); + } else { + resetAnalysisState(); + } + }; + + return ( + + {children || (resetAll ? "Reset All" : "Clear Data")} + + ); +}; + +export default ResetButton; diff --git a/src/components/shared/VisibilityControl.tsx b/src/components/shared/VisibilityControl.tsx index 81a6d50..46d0c4d 100644 --- a/src/components/shared/VisibilityControl.tsx +++ b/src/components/shared/VisibilityControl.tsx @@ -1,5 +1,5 @@ -import React, { useContext } from "react"; -import { SimulariumUiContext } from "../../simulation/context"; +import React from "react"; +import { useSimulariumUi } from "../../hooks/useSimulationContext"; import { Module, Section } from "../../types"; interface VisibilityControlProps { @@ -21,7 +21,7 @@ const VisibilityControl: React.FC = ({ notInIntroduction, startPage, }) => { - const { page, section, module } = useContext(SimulariumUiContext); + const { page, section, module } = useSimulariumUi(); if (conditionalRender === false) { return null; } diff --git a/src/hooks/useSimulationContext.ts b/src/hooks/useSimulationContext.ts new file mode 100644 index 0000000..68ee299 --- /dev/null +++ b/src/hooks/useSimulationContext.ts @@ -0,0 +1,30 @@ +import { useContext } from "react"; +import { + SimulariumAnalysisContext, + SimulariumAnalysisContextType, + SimulariumSimulationContext, + SimulariumSimulationContextType, + SimulariumUiContext, + SimulariumUiContextType, +} from "../simulation/context"; + +/** + * Hook to access UI-related context (page, module, view type, etc.) + */ +export const useSimulariumUi = (): SimulariumUiContextType => { + return useContext(SimulariumUiContext); +}; + +/** + * Hook to access simulation-related context (playback, viewport, trajectory, etc.) + */ +export const useSimulariumSimulation = (): SimulariumSimulationContextType => { + return useContext(SimulariumSimulationContext); +}; + +/** + * Hook to access analysis-related context (recorded data, reset functions, etc.) + */ +export const useSimulariumAnalysis = (): SimulariumAnalysisContextType => { + return useContext(SimulariumAnalysisContext); +}; From 01751c9d41d6fa601a1fa96849cdc7821f52b7c9 Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 27 Mar 2026 13:41:26 -0700 Subject: [PATCH 05/23] add claude settings --- .claude/settings.json | 79 +++++++++++++ .claude/skills/code-review/SKILL.md | 107 ++++++++++++++++++ .claude/skills/grill-me/SKILL.md | 8 ++ .../REFERENCE.md | 65 +++++++++++ .../improve-codebase-architecture/SKILL.md | 74 ++++++++++++ .claude/skills/pre-commit/REFERENCE.md | 81 +++++++++++++ .claude/skills/pre-commit/SKILL.md | 82 ++++++++++++++ .claude/skills/tdd/SKILL.md | 107 ++++++++++++++++++ .claude/skills/tdd/mocking.md | 25 ++++ .claude/skills/tdd/tests.md | 41 +++++++ CLAUDE.md | 76 +++++++++++++ 11 files changed, 745 insertions(+) create mode 100644 .claude/settings.json create mode 100644 .claude/skills/code-review/SKILL.md create mode 100644 .claude/skills/grill-me/SKILL.md create mode 100644 .claude/skills/improve-codebase-architecture/REFERENCE.md create mode 100644 .claude/skills/improve-codebase-architecture/SKILL.md create mode 100644 .claude/skills/pre-commit/REFERENCE.md create mode 100644 .claude/skills/pre-commit/SKILL.md create mode 100644 .claude/skills/tdd/SKILL.md create mode 100644 .claude/skills/tdd/mocking.md create mode 100644 .claude/skills/tdd/tests.md create mode 100644 CLAUDE.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f286989 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,79 @@ +{ + "permissions": { + "allow": [ + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git branch:*)", + "Bash(git worktree:*)", + "Bash(git show:*)", + "Bash(git stash:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git checkout:*)", + "Bash(git switch:*)", + "Bash(git merge:*)", + "Bash(git rebase:*)", + "Bash(git blame:*)", + "Bash(git remote:*)", + "Bash(git fetch:*)", + "Bash(git tag:*)", + "Bash(ls:*)", + "Bash(cat:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(wc:*)", + "Bash(sort:*)", + "Bash(uniq:*)", + "Bash(cut:*)", + "Bash(tr:*)", + "Bash(diff:*)", + "Bash(file:*)", + "Bash(stat:*)", + "Bash(mkdir:*)", + "Bash(cp:*)", + "Bash(mv:*)", + "Bash(touch:*)", + "Bash(echo:*)", + "Bash(pwd:*)", + "Bash(which:*)", + "Bash(find:*)", + "Bash(grep:*)", + "Bash(rg:*)", + "Bash(fd:*)", + "Bash(tree:*)", + "Bash(du:*)", + "Bash(env:*)", + "Bash(printenv:*)", + "Bash(date:*)", + "Bash(jq:*)", + "Bash(sed:*)", + "Bash(awk:*)", + "Bash(xargs:*)", + "Bash(tee:*)", + "Bash(basename:*)", + "Bash(dirname:*)", + "Bash(gh:*)", + "Bash(node:*)", + "Bash(npm:*)", + "Bash(npx:*)", + "Bash(bun:*)", + "Bash(bunx:*)", + "Bash(afplay:*)", + "Read", + "Edit", + "Write", + "Glob", + "Grep", + "Agent" + ], + "deny": [ + "Bash(rm -rf /:*)", + "Bash(rm -rf ~:*)", + "Bash(git push --force:*)", + "Bash(git reset --hard:*)", + "Bash(git clean -f:*)", + "Bash(git branch -D:*)" + ] + } +} diff --git a/.claude/skills/code-review/SKILL.md b/.claude/skills/code-review/SKILL.md new file mode 100644 index 0000000..24b0bd0 --- /dev/null +++ b/.claude/skills/code-review/SKILL.md @@ -0,0 +1,107 @@ +--- +name: code-review +description: Review a PR or set of changes for code quality, correctness, and team standards. Use when user wants a code review, PR review, asks to review changes, or mentions "review this". +--- + +# Code Review + +Perform a thorough code review on a PR or set of changes, producing actionable feedback organized by severity. + +## Process + +### 1. Gather the changes + +Determine what to review: +- If given a PR number/URL: `gh pr diff ` and `gh pr view ` +- If reviewing local changes: `git diff` or `git diff ` +- If given specific files: read those files + +Also read the PR description or task file for context on intent. + +### 2. Understand the context + +Before reviewing line-by-line: +- What problem is this solving? (read PR description, linked issues, task files) +- What are the acceptance criteria? +- Which files are test files vs production code? + +### 3. Review systematically + +Check each area in order of importance: + +#### Correctness +- Does the code do what the PR/task says it should? +- Are there logic errors, off-by-ones, missing edge cases? +- Are error conditions handled? +- Could this break existing functionality? + +#### Security +- Any user input used without validation/sanitization? +- Secrets or credentials exposed? +- SQL injection, XSS, command injection risks? +- Overly permissive access controls? + +#### Scope compliance +- Does the change stay within the stated goal? +- Any "while I'm here" changes that should be separate? +- Speculative features or premature abstractions? + +#### Redundancy & reuse +- Does new code duplicate existing utilities? +- Could existing helpers be used instead? +- Are there repeated patterns that should be extracted? + +#### Type safety & API design +- Are public interfaces well-typed? +- Are function signatures clear about what they accept and return? +- Any `any` types without justification? + +#### Test quality +- Do tests verify behavior through public interfaces? +- Are tests coupled to implementation details? +- Are critical paths covered? +- Do tests read like specifications? + +#### Style & consistency +- Does the code follow the patterns established in this repo? +- Naming conventions followed? +- File organization consistent with project structure? + +### 4. Present findings + +Organize feedback into three categories: + +**Must fix** — Issues that should block merge: +- Bugs, security issues, correctness problems +- Breaking changes without migration +- Missing tests for critical paths + +**Should fix** — Issues worth addressing but not blocking: +- Minor redundancy or missed reuse opportunities +- Style inconsistencies +- Weak typing that could be stronger + +**Nit** — Optional improvements: +- Naming suggestions +- Minor readability improvements +- Alternative approaches to consider + +For each finding: +- Reference the specific file and line(s) +- Explain *why* it's an issue (not just *what* to change) +- Suggest a concrete fix when possible + +### 5. Summary + +End with: +- Overall assessment (approve, request changes, or needs discussion) +- What the PR does well (acknowledge good work) +- The most important 1-2 items to address + +## Adapting for team use + +When used as part of a team code review workflow: +- Check the repo's `CLAUDE.md` for project-specific standards +- Reference team conventions when flagging issues +- Distinguish between objective issues (bugs) and subjective preferences (style) +- Be explicit about which standards come from the project vs general best practices diff --git a/.claude/skills/grill-me/SKILL.md b/.claude/skills/grill-me/SKILL.md new file mode 100644 index 0000000..f1543a9 --- /dev/null +++ b/.claude/skills/grill-me/SKILL.md @@ -0,0 +1,8 @@ +--- +name: grill-me +description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +If a question can be answered by exploring the codebase, explore the codebase instead. diff --git a/.claude/skills/improve-codebase-architecture/REFERENCE.md b/.claude/skills/improve-codebase-architecture/REFERENCE.md new file mode 100644 index 0000000..109eea5 --- /dev/null +++ b/.claude/skills/improve-codebase-architecture/REFERENCE.md @@ -0,0 +1,65 @@ +# Reference + +## Dependency Categories + +When assessing a candidate for deepening, classify its dependencies: + +### 1. In-process + +Pure computation, in-memory state, no I/O. Always deepenable — just merge the modules and test directly. + +### 2. Local-substitutable + +Dependencies that have local test stand-ins (e.g., PGLite for Postgres, in-memory filesystem). Deepenable if the test substitute exists. + +### 3. Remote but owned (Ports & Adapters) + +Your own services across a network boundary. Define a port (interface) at the module boundary. Tests use an in-memory adapter. Production uses the real adapter. + +### 4. True external (Mock) + +Third-party services you don't control. Mock at the boundary. The deepened module takes the external dependency as an injected port. + +## Testing Strategy + +**Replace, don't layer.** + +- Old unit tests on shallow modules are waste once boundary tests exist — delete them +- Write new tests at the deepened module's interface boundary +- Tests assert on observable outcomes through the public interface +- Tests should survive internal refactors + +## Issue Template + + + +## Problem + +- Which modules are shallow and tightly coupled +- What integration risk exists in the seams between them +- Why this makes the codebase harder to navigate and maintain + +## Proposed Interface + +- Interface signature (types, methods, params) +- Usage example showing how callers use it +- What complexity it hides internally + +## Dependency Strategy + +Which category applies and how dependencies are handled. + +## Testing Strategy + +- **New boundary tests to write**: behaviors to verify at the interface +- **Old tests to delete**: shallow module tests that become redundant +- **Test environment needs**: local stand-ins or adapters required + +## Implementation Recommendations + +- What the module should own (responsibilities) +- What it should hide (implementation details) +- What it should expose (the interface contract) +- How callers should migrate to the new interface + + diff --git a/.claude/skills/improve-codebase-architecture/SKILL.md b/.claude/skills/improve-codebase-architecture/SKILL.md new file mode 100644 index 0000000..8fd84c6 --- /dev/null +++ b/.claude/skills/improve-codebase-architecture/SKILL.md @@ -0,0 +1,74 @@ +--- +name: improve-codebase-architecture +description: Explore a codebase to find opportunities for architectural improvement, focusing on making the codebase more testable by deepening shallow modules. Use when user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more AI-navigable. +--- + +# Improve Codebase Architecture + +Explore a codebase like an AI would, surface architectural friction, discover opportunities for improving testability, and propose module-deepening refactors as GitHub issue RFCs. + +A **deep module** (John Ousterhout, "A Philosophy of Software Design") has a small interface hiding a large implementation. Deep modules are more testable, more AI-navigable, and let you test at the boundary instead of inside. + +## Process + +### 1. Explore the codebase + +Use the Agent tool with subagent_type=Explore to navigate the codebase naturally. Do NOT follow rigid heuristics — explore organically and note where you experience friction: + +- Where does understanding one concept require bouncing between many small files? +- Where are modules so shallow that the interface is nearly as complex as the implementation? +- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called? +- Where do tightly-coupled modules create integration risk in the seams between them? +- Which parts of the codebase are untested, or hard to test? + +The friction you encounter IS the signal. + +### 2. Present candidates + +Present a numbered list of deepening opportunities. For each candidate, show: + +- **Cluster**: Which modules/concepts are involved +- **Why they're coupled**: Shared types, call patterns, co-ownership of a concept +- **Dependency category**: See [REFERENCE.md](REFERENCE.md) for the four categories +- **Test impact**: What existing tests would be replaced by boundary tests + +Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?" + +### 3. User picks a candidate + +### 4. Frame the problem space + +Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: + +- The constraints any new interface would need to satisfy +- The dependencies it would need to rely on +- A rough illustrative code sketch to make the constraints concrete — this is not a proposal, just a way to ground the constraints + +Show this to the user, then immediately proceed to Step 5. + +### 5. Design multiple interfaces + +Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module. + +Give each agent a different design constraint: + +- Agent 1: "Minimize the interface — aim for 1-3 entry points max" +- Agent 2: "Maximize flexibility — support many use cases and extension" +- Agent 3: "Optimize for the most common caller — make the default case trivial" +- Agent 4 (if applicable): "Design around the ports & adapters pattern for cross-boundary dependencies" + +Each sub-agent outputs: + +1. Interface signature (types, methods, params) +2. Usage example showing how callers use it +3. What complexity it hides internally +4. Dependency strategy (how deps are handled — see [REFERENCE.md](REFERENCE.md)) +5. Trade-offs + +Present designs sequentially, then compare them in prose. Give your own recommendation. + +### 6. User picks an interface (or accepts recommendation) + +### 7. Create GitHub issue + +Create a refactor RFC as a GitHub issue using `gh issue create`. Use the template in [REFERENCE.md](REFERENCE.md). diff --git a/.claude/skills/pre-commit/REFERENCE.md b/.claude/skills/pre-commit/REFERENCE.md new file mode 100644 index 0000000..13bee26 --- /dev/null +++ b/.claude/skills/pre-commit/REFERENCE.md @@ -0,0 +1,81 @@ +# Pre-Commit Reference: Check Detection & Execution + +## Detecting the repo's check system + +Check for these in order — use the first match found: + +| File | System | Run checks with | +|------|--------|----------------| +| `lefthook.yml` | Lefthook | `npx lefthook run pre-commit` | +| `.husky/pre-commit` | Husky | Read the hook file, run its commands directly | +| `.pre-commit-config.yaml` | pre-commit (Python) | `pre-commit run --all-files` | +| `Makefile` with `lint`/`check` targets | Make | `make lint`, `make check`, etc. | +| `.github/workflows/*.yml` | GitHub Actions (no local runner) | Extract commands from workflow steps | +| `package.json` scripts | npm scripts | Run matching scripts directly | + +If no hook system is found, fall back to detecting individual tools. + +## Detecting individual checks + +### Format + +| Look for | Tool | Command | +|----------|------|---------| +| `.prettierrc*`, `prettier` in deps | Prettier | `npx prettier --write .` | +| `pyproject.toml` with `[tool.black]` | Black | `black .` | +| `pyproject.toml` with `[tool.ruff.format]` | Ruff format | `ruff format .` | + +### Lint + +| Look for | Tool | Command | +|----------|------|---------| +| `.eslintrc*` or `eslint` in deps | ESLint | `npx eslint --fix .` | +| `pyproject.toml` with `[tool.ruff]` | Ruff | `ruff check --fix .` | + +### Type check + +| Look for | Tool | Command | +|----------|------|---------| +| `tsconfig.json` | TypeScript | `bunx tsc --noEmit` | + +### Test + +| Look for | Tool | Command | +|----------|------|---------| +| `jest.config.*` or `jest` in deps | Jest | `npx jest` | +| `vitest.config.*` or `vitest` in deps | Vitest | `npx vitest run` | + +### Build + +| Look for | Tool | Command | +|----------|------|---------| +| `build` script in `package.json` | bun | `bun run build` | + +## Execution order and logic + +``` +1. Format (auto-fixes → re-stage changed files) +2. Lint (auto-fixes → re-stage changed files) +3. Type check +4. Test +5. Build +``` + +After format and lint steps, if files were modified: +```bash +git add -u # re-stage auto-fixed files +``` + +## Handling check failures + +| Failure type | Action | +|-------------|--------| +| Format diff | Auto-fixed by formatter, re-stage | +| Lint auto-fixable | Auto-fixed by linter, re-stage | +| Lint error (not auto-fixable) | Read the error, fix the code, re-run | +| Type error | Read the error, fix the code, re-run | +| Test failure | Read the failure, fix the code, re-run tests | +| Build failure | Read the error, fix the code, re-run build | +| Unclear / complex failure | Stop and ask the user | + +Maximum retry per check: 3 attempts. If still failing after 3, ask the user. diff --git a/.claude/skills/pre-commit/SKILL.md b/.claude/skills/pre-commit/SKILL.md new file mode 100644 index 0000000..eac1c8c --- /dev/null +++ b/.claude/skills/pre-commit/SKILL.md @@ -0,0 +1,82 @@ +--- +name: pre-commit +description: Pre-commit validation that updates docs (README, CONTRIBUTING, docs/, tasks/) to reflect working tree changes and runs repo checks before committing. Use when user says "ready to commit", "let's commit", asks Claude to commit, or wants to finalize changes. +--- + +# Pre-Commit + +Ensure documentation stays in sync with code and all repo checks pass before committing. + +## Two Modes + +1. **"Ready to commit"** — user will commit themselves. Update docs only. +2. **"Commit this"** — Claude commits. Update docs, then run all checks, then commit. + +## Workflow + +### 1. Assess the working tree + +```bash +git status +git diff --stat +git diff # staged + unstaged +``` + +Identify what changed: new files, renamed files, deleted files, modified modules, changed APIs, new dependencies, config changes. + +### 2. Update documentation + +For each doc target, compare the current doc content against the working tree changes. Only touch sections that are actually stale or missing — do not rewrite docs that are already accurate. + +#### README.md +- [ ] Project description still accurate? +- [ ] Setup / install instructions match current deps and scripts? +- [ ] Usage examples reflect current API / CLI? +- [ ] Feature list includes new capabilities, removes deleted ones? +- [ ] Any new env vars, config files, or prerequisites to document? + +#### CONTRIBUTING.md (if exists) +- [ ] Dev setup steps still work? +- [ ] Testing instructions match current test runner / commands? +- [ ] Any new conventions introduced by these changes? + +#### docs/ directory (if exists) +- [ ] Architecture docs reflect structural changes? +- [ ] API docs match changed interfaces? +- [ ] Any new docs needed for new modules or features? +- [ ] Remove docs for deleted features? + +#### tasks/ directory (if exists) +- [ ] Mark completed tasks as done +- [ ] Update in-progress tasks with current state +- [ ] Note any new tasks discovered during this work + +### 3. Run repo checks (commit mode only) + +Detect and run the repo's pre-commit checks. See [REFERENCE.md](REFERENCE.md) for detection logic and execution details. + +Sequence: +1. **Format** — auto-fix formatting (Prettier, Black, etc.) +2. **Lint** — run linters, fix auto-fixable issues +3. **Type check** — run type checker if configured +4. **Test** — run test suite +5. **Build** — verify build succeeds (if applicable) + +If any check fails: +- Fix the issue +- Re-run the failing check +- Continue to the next check +- If a fix is non-trivial, stop and ask the user + +### 4. Commit (commit mode only) + +- Stage all relevant changes (including doc updates) +- Write a commit message that covers both code and doc changes +- Let the commit run through the repo's git hooks naturally + +## Key Principles + +- **Don't invent documentation** — only document what exists in the code +- **Don't rewrite clean docs** — if a section is already accurate, leave it alone +- **Fix, don't skip** — if a check fails, fix it before moving on +- **Ask when stuck** — if a check failure isn't straightforward, ask the user diff --git a/.claude/skills/tdd/SKILL.md b/.claude/skills/tdd/SKILL.md new file mode 100644 index 0000000..22bdf09 --- /dev/null +++ b/.claude/skills/tdd/SKILL.md @@ -0,0 +1,107 @@ +--- +name: tdd +description: Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development. +--- + +# Test-Driven Development + +## Philosophy + +**Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't. + +**Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure. + +**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior. + +See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines. + +## Anti-Pattern: Horizontal Slices + +**DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code." + +This produces **crap tests**: + +- Tests written in bulk test _imagined_ behavior, not _actual_ behavior +- You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior +- Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine +- You outrun your headlights, committing to test structure before understanding the implementation + +**Correct approach**: Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. + +``` +WRONG (horizontal): + RED: test1, test2, test3, test4, test5 + GREEN: impl1, impl2, impl3, impl4, impl5 + +RIGHT (vertical): + RED→GREEN: test1→impl1 + RED→GREEN: test2→impl2 + RED→GREEN: test3→impl3 + ... +``` + +## Workflow + +### 1. Planning + +Before writing any code: + +- [ ] Confirm with user what interface changes are needed +- [ ] Confirm with user which behaviors to test (prioritize) +- [ ] Identify opportunities for deep modules (small interface, deep implementation) +- [ ] Design interfaces for testability +- [ ] List the behaviors to test (not implementation steps) +- [ ] Get user approval on the plan + +Ask: "What should the public interface look like? Which behaviors are most important to test?" + +**You can't test everything.** Confirm with the user exactly which behaviors matter most. + +### 2. Tracer Bullet + +Write ONE test that confirms ONE thing about the system: + +``` +RED: Write test for first behavior → test fails +GREEN: Write minimal code to pass → test passes +``` + +This is your tracer bullet - proves the path works end-to-end. + +### 3. Incremental Loop + +For each remaining behavior: + +``` +RED: Write next test → fails +GREEN: Minimal code to pass → passes +``` + +Rules: + +- One test at a time +- Only enough code to pass current test +- Don't anticipate future tests +- Keep tests focused on observable behavior + +### 4. Refactor + +After all tests pass, look for refactor candidates: + +- [ ] Extract duplication +- [ ] Deepen modules (move complexity behind simple interfaces) +- [ ] Apply SOLID principles where natural +- [ ] Consider what new code reveals about existing code +- [ ] Run tests after each refactor step + +**Never refactor while RED.** Get to GREEN first. + +## Checklist Per Cycle + +``` +[ ] Test describes behavior, not implementation +[ ] Test uses public interface only +[ ] Test would survive internal refactor +[ ] Code is minimal for this test +[ ] No speculative features added +``` diff --git a/.claude/skills/tdd/mocking.md b/.claude/skills/tdd/mocking.md new file mode 100644 index 0000000..30423e9 --- /dev/null +++ b/.claude/skills/tdd/mocking.md @@ -0,0 +1,25 @@ +# Mocking Guidelines + +## When to Mock + +Mock only at system boundaries — things you don't own or can't run in tests: + +- **External APIs** (Stripe, Twilio, third-party services) +- **Network calls** to services outside your test environment +- **System resources** (filesystem in CI, hardware interfaces) + +## When NOT to Mock + +Never mock internal collaborators: + +- Your own modules, classes, or functions +- Database access (use a test database or in-memory alternative) +- Internal services within the same codebase + +If you feel the need to mock an internal module, that's a signal the interface boundary is wrong — consider deepening the module instead. + +## The Rule + +> If you wrote it, don't mock it. Test through it. + +Mocking internal code creates tests that pass when behavior is broken and fail when behavior is fine. The mock becomes a parallel implementation that drifts from reality. diff --git a/.claude/skills/tdd/tests.md b/.claude/skills/tdd/tests.md new file mode 100644 index 0000000..3d2181c --- /dev/null +++ b/.claude/skills/tdd/tests.md @@ -0,0 +1,41 @@ +# Test Examples + +## Good Tests + +Good tests describe behavior through the public interface: + +```typescript +test("user can checkout with valid cart", async () => { + const cart = createCart(); + cart.addItem({ id: "shirt", quantity: 2, price: 25 }); + const result = await checkout(cart, validPayment); + expect(result.status).toBe("confirmed"); + expect(result.total).toBe(50); +}); +``` + +This test: +- Uses the public API (`createCart`, `addItem`, `checkout`) +- Asserts on observable outcomes (`status`, `total`) +- Reads like a specification +- Would survive any internal refactor + +## Bad Tests + +Bad tests are coupled to implementation: + +```typescript +test("checkout calls payment gateway", async () => { + const mockGateway = jest.fn().mockResolvedValue({ ok: true }); + const cart = new Cart(); + cart._items = [{ id: "shirt", qty: 2 }]; // accessing internals + await cart._processCheckout(mockGateway); // testing private method + expect(mockGateway).toHaveBeenCalledWith(/* specific internal format */); +}); +``` + +Red flags: +- Accesses private/internal state (`_items`, `_processCheckout`) +- Mocks an internal collaborator +- Asserts on *how* it works, not *what* it produces +- Will break if internals change, even if behavior is identical diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..db8f0f1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# binding-sim-edu — Claude Code Guidelines + +An educational web app for exploring molecular binding simulations, built for the Allen Institute for Cell Science. Runs interactive binding simulations via the Simularium viewer, guides students through experiments, and displays analysis results (concentration plots, equilibrium graphs). + +--- + +## Quick Reference + +``` +binding-sim-edu/ +├── CLAUDE.md <- this file +├── CONTEXT_ORGANIZATION.md <- context/state architecture docs +├── src/ +│ ├── components/ <- UI components +│ ├── hooks/ <- React hooks (incl. useSimulationContext.ts) +│ ├── simulation/ <- simulation engine logic +│ ├── constants/ <- app constants +│ ├── content/ <- module content / copy +│ ├── types/ <- shared TypeScript types +│ └── utils/ <- pure utility functions +└── .claude/skills/ <- Claude Code skills +``` + +--- + +## State Architecture + +Context is split into three providers — see `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 + +Always import via the typed hooks, not `useContext` directly: +```tsx +import { useSimulariumUi, useSimulariumSimulation, useSimulariumAnalysis } from "../hooks/useSimulationContext"; +``` + +--- + +## Build Commands + +| What | Command | +|------|---------| +| Dev server | `bun run dev` | +| Build | `bun run build` | +| Lint | `bun run lint` | +| Type check | `bunx tsc --noEmit` | + +--- + +## Code Standards + +### General +- Prefer editing existing files over creating new ones +- No speculative abstractions — build for what's needed now +- No commented-out code +- No `console.log` / debug prints left in production code +- Destructured keys in alphabetical order (imports, params, props) + +### TypeScript / React +- All public APIs must have explicit types +- Prefer `interface` over `type` for object shapes +- No `any` — use `unknown` + narrowing if type is genuinely unknown +- Components own their props types — don't scatter them into shared types files +- Custom hooks for shared stateful logic, plain functions for shared pure logic +- Co-locate component, styles, and tests in the same directory + +### Import order +External libs → internal aliases → relative imports, each group alphabetical. + +--- + +## Notification + +After finishing work, run: `afplay /System/Library/Sounds/Funk.aiff` From 93669f9f38f237a051df492f019c2e4ed1c00a5e Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 21 May 2026 15:23:17 -0700 Subject: [PATCH 06/23] ignore claude settings --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a547bf3..117dc86 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.claude \ No newline at end of file From e0b9f9507793b4cd411601585aab00b5bc1b5a29 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 21 May 2026 15:24:38 -0700 Subject: [PATCH 07/23] remove claude settings --- .claude/settings.json | 79 ------------- .claude/skills/code-review/SKILL.md | 107 ------------------ .claude/skills/grill-me/SKILL.md | 8 -- .../REFERENCE.md | 65 ----------- .../improve-codebase-architecture/SKILL.md | 74 ------------ .claude/skills/pre-commit/REFERENCE.md | 81 ------------- .claude/skills/pre-commit/SKILL.md | 82 -------------- .claude/skills/tdd/SKILL.md | 107 ------------------ .claude/skills/tdd/mocking.md | 25 ---- .claude/skills/tdd/tests.md | 41 ------- 10 files changed, 669 deletions(-) delete mode 100644 .claude/settings.json delete mode 100644 .claude/skills/code-review/SKILL.md delete mode 100644 .claude/skills/grill-me/SKILL.md delete mode 100644 .claude/skills/improve-codebase-architecture/REFERENCE.md delete mode 100644 .claude/skills/improve-codebase-architecture/SKILL.md delete mode 100644 .claude/skills/pre-commit/REFERENCE.md delete mode 100644 .claude/skills/pre-commit/SKILL.md delete mode 100644 .claude/skills/tdd/SKILL.md delete mode 100644 .claude/skills/tdd/mocking.md delete mode 100644 .claude/skills/tdd/tests.md diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index f286989..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(git status:*)", - "Bash(git diff:*)", - "Bash(git log:*)", - "Bash(git branch:*)", - "Bash(git worktree:*)", - "Bash(git show:*)", - "Bash(git stash:*)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(git checkout:*)", - "Bash(git switch:*)", - "Bash(git merge:*)", - "Bash(git rebase:*)", - "Bash(git blame:*)", - "Bash(git remote:*)", - "Bash(git fetch:*)", - "Bash(git tag:*)", - "Bash(ls:*)", - "Bash(cat:*)", - "Bash(head:*)", - "Bash(tail:*)", - "Bash(wc:*)", - "Bash(sort:*)", - "Bash(uniq:*)", - "Bash(cut:*)", - "Bash(tr:*)", - "Bash(diff:*)", - "Bash(file:*)", - "Bash(stat:*)", - "Bash(mkdir:*)", - "Bash(cp:*)", - "Bash(mv:*)", - "Bash(touch:*)", - "Bash(echo:*)", - "Bash(pwd:*)", - "Bash(which:*)", - "Bash(find:*)", - "Bash(grep:*)", - "Bash(rg:*)", - "Bash(fd:*)", - "Bash(tree:*)", - "Bash(du:*)", - "Bash(env:*)", - "Bash(printenv:*)", - "Bash(date:*)", - "Bash(jq:*)", - "Bash(sed:*)", - "Bash(awk:*)", - "Bash(xargs:*)", - "Bash(tee:*)", - "Bash(basename:*)", - "Bash(dirname:*)", - "Bash(gh:*)", - "Bash(node:*)", - "Bash(npm:*)", - "Bash(npx:*)", - "Bash(bun:*)", - "Bash(bunx:*)", - "Bash(afplay:*)", - "Read", - "Edit", - "Write", - "Glob", - "Grep", - "Agent" - ], - "deny": [ - "Bash(rm -rf /:*)", - "Bash(rm -rf ~:*)", - "Bash(git push --force:*)", - "Bash(git reset --hard:*)", - "Bash(git clean -f:*)", - "Bash(git branch -D:*)" - ] - } -} diff --git a/.claude/skills/code-review/SKILL.md b/.claude/skills/code-review/SKILL.md deleted file mode 100644 index 24b0bd0..0000000 --- a/.claude/skills/code-review/SKILL.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: code-review -description: Review a PR or set of changes for code quality, correctness, and team standards. Use when user wants a code review, PR review, asks to review changes, or mentions "review this". ---- - -# Code Review - -Perform a thorough code review on a PR or set of changes, producing actionable feedback organized by severity. - -## Process - -### 1. Gather the changes - -Determine what to review: -- If given a PR number/URL: `gh pr diff ` and `gh pr view ` -- If reviewing local changes: `git diff` or `git diff ` -- If given specific files: read those files - -Also read the PR description or task file for context on intent. - -### 2. Understand the context - -Before reviewing line-by-line: -- What problem is this solving? (read PR description, linked issues, task files) -- What are the acceptance criteria? -- Which files are test files vs production code? - -### 3. Review systematically - -Check each area in order of importance: - -#### Correctness -- Does the code do what the PR/task says it should? -- Are there logic errors, off-by-ones, missing edge cases? -- Are error conditions handled? -- Could this break existing functionality? - -#### Security -- Any user input used without validation/sanitization? -- Secrets or credentials exposed? -- SQL injection, XSS, command injection risks? -- Overly permissive access controls? - -#### Scope compliance -- Does the change stay within the stated goal? -- Any "while I'm here" changes that should be separate? -- Speculative features or premature abstractions? - -#### Redundancy & reuse -- Does new code duplicate existing utilities? -- Could existing helpers be used instead? -- Are there repeated patterns that should be extracted? - -#### Type safety & API design -- Are public interfaces well-typed? -- Are function signatures clear about what they accept and return? -- Any `any` types without justification? - -#### Test quality -- Do tests verify behavior through public interfaces? -- Are tests coupled to implementation details? -- Are critical paths covered? -- Do tests read like specifications? - -#### Style & consistency -- Does the code follow the patterns established in this repo? -- Naming conventions followed? -- File organization consistent with project structure? - -### 4. Present findings - -Organize feedback into three categories: - -**Must fix** — Issues that should block merge: -- Bugs, security issues, correctness problems -- Breaking changes without migration -- Missing tests for critical paths - -**Should fix** — Issues worth addressing but not blocking: -- Minor redundancy or missed reuse opportunities -- Style inconsistencies -- Weak typing that could be stronger - -**Nit** — Optional improvements: -- Naming suggestions -- Minor readability improvements -- Alternative approaches to consider - -For each finding: -- Reference the specific file and line(s) -- Explain *why* it's an issue (not just *what* to change) -- Suggest a concrete fix when possible - -### 5. Summary - -End with: -- Overall assessment (approve, request changes, or needs discussion) -- What the PR does well (acknowledge good work) -- The most important 1-2 items to address - -## Adapting for team use - -When used as part of a team code review workflow: -- Check the repo's `CLAUDE.md` for project-specific standards -- Reference team conventions when flagging issues -- Distinguish between objective issues (bugs) and subjective preferences (style) -- Be explicit about which standards come from the project vs general best practices diff --git a/.claude/skills/grill-me/SKILL.md b/.claude/skills/grill-me/SKILL.md deleted file mode 100644 index f1543a9..0000000 --- a/.claude/skills/grill-me/SKILL.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: grill-me -description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". ---- - -Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. - -If a question can be answered by exploring the codebase, explore the codebase instead. diff --git a/.claude/skills/improve-codebase-architecture/REFERENCE.md b/.claude/skills/improve-codebase-architecture/REFERENCE.md deleted file mode 100644 index 109eea5..0000000 --- a/.claude/skills/improve-codebase-architecture/REFERENCE.md +++ /dev/null @@ -1,65 +0,0 @@ -# Reference - -## Dependency Categories - -When assessing a candidate for deepening, classify its dependencies: - -### 1. In-process - -Pure computation, in-memory state, no I/O. Always deepenable — just merge the modules and test directly. - -### 2. Local-substitutable - -Dependencies that have local test stand-ins (e.g., PGLite for Postgres, in-memory filesystem). Deepenable if the test substitute exists. - -### 3. Remote but owned (Ports & Adapters) - -Your own services across a network boundary. Define a port (interface) at the module boundary. Tests use an in-memory adapter. Production uses the real adapter. - -### 4. True external (Mock) - -Third-party services you don't control. Mock at the boundary. The deepened module takes the external dependency as an injected port. - -## Testing Strategy - -**Replace, don't layer.** - -- Old unit tests on shallow modules are waste once boundary tests exist — delete them -- Write new tests at the deepened module's interface boundary -- Tests assert on observable outcomes through the public interface -- Tests should survive internal refactors - -## Issue Template - - - -## Problem - -- Which modules are shallow and tightly coupled -- What integration risk exists in the seams between them -- Why this makes the codebase harder to navigate and maintain - -## Proposed Interface - -- Interface signature (types, methods, params) -- Usage example showing how callers use it -- What complexity it hides internally - -## Dependency Strategy - -Which category applies and how dependencies are handled. - -## Testing Strategy - -- **New boundary tests to write**: behaviors to verify at the interface -- **Old tests to delete**: shallow module tests that become redundant -- **Test environment needs**: local stand-ins or adapters required - -## Implementation Recommendations - -- What the module should own (responsibilities) -- What it should hide (implementation details) -- What it should expose (the interface contract) -- How callers should migrate to the new interface - - diff --git a/.claude/skills/improve-codebase-architecture/SKILL.md b/.claude/skills/improve-codebase-architecture/SKILL.md deleted file mode 100644 index 8fd84c6..0000000 --- a/.claude/skills/improve-codebase-architecture/SKILL.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -name: improve-codebase-architecture -description: Explore a codebase to find opportunities for architectural improvement, focusing on making the codebase more testable by deepening shallow modules. Use when user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more AI-navigable. ---- - -# Improve Codebase Architecture - -Explore a codebase like an AI would, surface architectural friction, discover opportunities for improving testability, and propose module-deepening refactors as GitHub issue RFCs. - -A **deep module** (John Ousterhout, "A Philosophy of Software Design") has a small interface hiding a large implementation. Deep modules are more testable, more AI-navigable, and let you test at the boundary instead of inside. - -## Process - -### 1. Explore the codebase - -Use the Agent tool with subagent_type=Explore to navigate the codebase naturally. Do NOT follow rigid heuristics — explore organically and note where you experience friction: - -- Where does understanding one concept require bouncing between many small files? -- Where are modules so shallow that the interface is nearly as complex as the implementation? -- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called? -- Where do tightly-coupled modules create integration risk in the seams between them? -- Which parts of the codebase are untested, or hard to test? - -The friction you encounter IS the signal. - -### 2. Present candidates - -Present a numbered list of deepening opportunities. For each candidate, show: - -- **Cluster**: Which modules/concepts are involved -- **Why they're coupled**: Shared types, call patterns, co-ownership of a concept -- **Dependency category**: See [REFERENCE.md](REFERENCE.md) for the four categories -- **Test impact**: What existing tests would be replaced by boundary tests - -Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?" - -### 3. User picks a candidate - -### 4. Frame the problem space - -Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: - -- The constraints any new interface would need to satisfy -- The dependencies it would need to rely on -- A rough illustrative code sketch to make the constraints concrete — this is not a proposal, just a way to ground the constraints - -Show this to the user, then immediately proceed to Step 5. - -### 5. Design multiple interfaces - -Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module. - -Give each agent a different design constraint: - -- Agent 1: "Minimize the interface — aim for 1-3 entry points max" -- Agent 2: "Maximize flexibility — support many use cases and extension" -- Agent 3: "Optimize for the most common caller — make the default case trivial" -- Agent 4 (if applicable): "Design around the ports & adapters pattern for cross-boundary dependencies" - -Each sub-agent outputs: - -1. Interface signature (types, methods, params) -2. Usage example showing how callers use it -3. What complexity it hides internally -4. Dependency strategy (how deps are handled — see [REFERENCE.md](REFERENCE.md)) -5. Trade-offs - -Present designs sequentially, then compare them in prose. Give your own recommendation. - -### 6. User picks an interface (or accepts recommendation) - -### 7. Create GitHub issue - -Create a refactor RFC as a GitHub issue using `gh issue create`. Use the template in [REFERENCE.md](REFERENCE.md). diff --git a/.claude/skills/pre-commit/REFERENCE.md b/.claude/skills/pre-commit/REFERENCE.md deleted file mode 100644 index 13bee26..0000000 --- a/.claude/skills/pre-commit/REFERENCE.md +++ /dev/null @@ -1,81 +0,0 @@ -# Pre-Commit Reference: Check Detection & Execution - -## Detecting the repo's check system - -Check for these in order — use the first match found: - -| File | System | Run checks with | -|------|--------|----------------| -| `lefthook.yml` | Lefthook | `npx lefthook run pre-commit` | -| `.husky/pre-commit` | Husky | Read the hook file, run its commands directly | -| `.pre-commit-config.yaml` | pre-commit (Python) | `pre-commit run --all-files` | -| `Makefile` with `lint`/`check` targets | Make | `make lint`, `make check`, etc. | -| `.github/workflows/*.yml` | GitHub Actions (no local runner) | Extract commands from workflow steps | -| `package.json` scripts | npm scripts | Run matching scripts directly | - -If no hook system is found, fall back to detecting individual tools. - -## Detecting individual checks - -### Format - -| Look for | Tool | Command | -|----------|------|---------| -| `.prettierrc*`, `prettier` in deps | Prettier | `npx prettier --write .` | -| `pyproject.toml` with `[tool.black]` | Black | `black .` | -| `pyproject.toml` with `[tool.ruff.format]` | Ruff format | `ruff format .` | - -### Lint - -| Look for | Tool | Command | -|----------|------|---------| -| `.eslintrc*` or `eslint` in deps | ESLint | `npx eslint --fix .` | -| `pyproject.toml` with `[tool.ruff]` | Ruff | `ruff check --fix .` | - -### Type check - -| Look for | Tool | Command | -|----------|------|---------| -| `tsconfig.json` | TypeScript | `bunx tsc --noEmit` | - -### Test - -| Look for | Tool | Command | -|----------|------|---------| -| `jest.config.*` or `jest` in deps | Jest | `npx jest` | -| `vitest.config.*` or `vitest` in deps | Vitest | `npx vitest run` | - -### Build - -| Look for | Tool | Command | -|----------|------|---------| -| `build` script in `package.json` | bun | `bun run build` | - -## Execution order and logic - -``` -1. Format (auto-fixes → re-stage changed files) -2. Lint (auto-fixes → re-stage changed files) -3. Type check -4. Test -5. Build -``` - -After format and lint steps, if files were modified: -```bash -git add -u # re-stage auto-fixed files -``` - -## Handling check failures - -| Failure type | Action | -|-------------|--------| -| Format diff | Auto-fixed by formatter, re-stage | -| Lint auto-fixable | Auto-fixed by linter, re-stage | -| Lint error (not auto-fixable) | Read the error, fix the code, re-run | -| Type error | Read the error, fix the code, re-run | -| Test failure | Read the failure, fix the code, re-run tests | -| Build failure | Read the error, fix the code, re-run build | -| Unclear / complex failure | Stop and ask the user | - -Maximum retry per check: 3 attempts. If still failing after 3, ask the user. diff --git a/.claude/skills/pre-commit/SKILL.md b/.claude/skills/pre-commit/SKILL.md deleted file mode 100644 index eac1c8c..0000000 --- a/.claude/skills/pre-commit/SKILL.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -name: pre-commit -description: Pre-commit validation that updates docs (README, CONTRIBUTING, docs/, tasks/) to reflect working tree changes and runs repo checks before committing. Use when user says "ready to commit", "let's commit", asks Claude to commit, or wants to finalize changes. ---- - -# Pre-Commit - -Ensure documentation stays in sync with code and all repo checks pass before committing. - -## Two Modes - -1. **"Ready to commit"** — user will commit themselves. Update docs only. -2. **"Commit this"** — Claude commits. Update docs, then run all checks, then commit. - -## Workflow - -### 1. Assess the working tree - -```bash -git status -git diff --stat -git diff # staged + unstaged -``` - -Identify what changed: new files, renamed files, deleted files, modified modules, changed APIs, new dependencies, config changes. - -### 2. Update documentation - -For each doc target, compare the current doc content against the working tree changes. Only touch sections that are actually stale or missing — do not rewrite docs that are already accurate. - -#### README.md -- [ ] Project description still accurate? -- [ ] Setup / install instructions match current deps and scripts? -- [ ] Usage examples reflect current API / CLI? -- [ ] Feature list includes new capabilities, removes deleted ones? -- [ ] Any new env vars, config files, or prerequisites to document? - -#### CONTRIBUTING.md (if exists) -- [ ] Dev setup steps still work? -- [ ] Testing instructions match current test runner / commands? -- [ ] Any new conventions introduced by these changes? - -#### docs/ directory (if exists) -- [ ] Architecture docs reflect structural changes? -- [ ] API docs match changed interfaces? -- [ ] Any new docs needed for new modules or features? -- [ ] Remove docs for deleted features? - -#### tasks/ directory (if exists) -- [ ] Mark completed tasks as done -- [ ] Update in-progress tasks with current state -- [ ] Note any new tasks discovered during this work - -### 3. Run repo checks (commit mode only) - -Detect and run the repo's pre-commit checks. See [REFERENCE.md](REFERENCE.md) for detection logic and execution details. - -Sequence: -1. **Format** — auto-fix formatting (Prettier, Black, etc.) -2. **Lint** — run linters, fix auto-fixable issues -3. **Type check** — run type checker if configured -4. **Test** — run test suite -5. **Build** — verify build succeeds (if applicable) - -If any check fails: -- Fix the issue -- Re-run the failing check -- Continue to the next check -- If a fix is non-trivial, stop and ask the user - -### 4. Commit (commit mode only) - -- Stage all relevant changes (including doc updates) -- Write a commit message that covers both code and doc changes -- Let the commit run through the repo's git hooks naturally - -## Key Principles - -- **Don't invent documentation** — only document what exists in the code -- **Don't rewrite clean docs** — if a section is already accurate, leave it alone -- **Fix, don't skip** — if a check fails, fix it before moving on -- **Ask when stuck** — if a check failure isn't straightforward, ask the user diff --git a/.claude/skills/tdd/SKILL.md b/.claude/skills/tdd/SKILL.md deleted file mode 100644 index 22bdf09..0000000 --- a/.claude/skills/tdd/SKILL.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: tdd -description: Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development. ---- - -# Test-Driven Development - -## Philosophy - -**Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't. - -**Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure. - -**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior. - -See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines. - -## Anti-Pattern: Horizontal Slices - -**DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code." - -This produces **crap tests**: - -- Tests written in bulk test _imagined_ behavior, not _actual_ behavior -- You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior -- Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine -- You outrun your headlights, committing to test structure before understanding the implementation - -**Correct approach**: Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. - -``` -WRONG (horizontal): - RED: test1, test2, test3, test4, test5 - GREEN: impl1, impl2, impl3, impl4, impl5 - -RIGHT (vertical): - RED→GREEN: test1→impl1 - RED→GREEN: test2→impl2 - RED→GREEN: test3→impl3 - ... -``` - -## Workflow - -### 1. Planning - -Before writing any code: - -- [ ] Confirm with user what interface changes are needed -- [ ] Confirm with user which behaviors to test (prioritize) -- [ ] Identify opportunities for deep modules (small interface, deep implementation) -- [ ] Design interfaces for testability -- [ ] List the behaviors to test (not implementation steps) -- [ ] Get user approval on the plan - -Ask: "What should the public interface look like? Which behaviors are most important to test?" - -**You can't test everything.** Confirm with the user exactly which behaviors matter most. - -### 2. Tracer Bullet - -Write ONE test that confirms ONE thing about the system: - -``` -RED: Write test for first behavior → test fails -GREEN: Write minimal code to pass → test passes -``` - -This is your tracer bullet - proves the path works end-to-end. - -### 3. Incremental Loop - -For each remaining behavior: - -``` -RED: Write next test → fails -GREEN: Minimal code to pass → passes -``` - -Rules: - -- One test at a time -- Only enough code to pass current test -- Don't anticipate future tests -- Keep tests focused on observable behavior - -### 4. Refactor - -After all tests pass, look for refactor candidates: - -- [ ] Extract duplication -- [ ] Deepen modules (move complexity behind simple interfaces) -- [ ] Apply SOLID principles where natural -- [ ] Consider what new code reveals about existing code -- [ ] Run tests after each refactor step - -**Never refactor while RED.** Get to GREEN first. - -## Checklist Per Cycle - -``` -[ ] Test describes behavior, not implementation -[ ] Test uses public interface only -[ ] Test would survive internal refactor -[ ] Code is minimal for this test -[ ] No speculative features added -``` diff --git a/.claude/skills/tdd/mocking.md b/.claude/skills/tdd/mocking.md deleted file mode 100644 index 30423e9..0000000 --- a/.claude/skills/tdd/mocking.md +++ /dev/null @@ -1,25 +0,0 @@ -# Mocking Guidelines - -## When to Mock - -Mock only at system boundaries — things you don't own or can't run in tests: - -- **External APIs** (Stripe, Twilio, third-party services) -- **Network calls** to services outside your test environment -- **System resources** (filesystem in CI, hardware interfaces) - -## When NOT to Mock - -Never mock internal collaborators: - -- Your own modules, classes, or functions -- Database access (use a test database or in-memory alternative) -- Internal services within the same codebase - -If you feel the need to mock an internal module, that's a signal the interface boundary is wrong — consider deepening the module instead. - -## The Rule - -> If you wrote it, don't mock it. Test through it. - -Mocking internal code creates tests that pass when behavior is broken and fail when behavior is fine. The mock becomes a parallel implementation that drifts from reality. diff --git a/.claude/skills/tdd/tests.md b/.claude/skills/tdd/tests.md deleted file mode 100644 index 3d2181c..0000000 --- a/.claude/skills/tdd/tests.md +++ /dev/null @@ -1,41 +0,0 @@ -# Test Examples - -## Good Tests - -Good tests describe behavior through the public interface: - -```typescript -test("user can checkout with valid cart", async () => { - const cart = createCart(); - cart.addItem({ id: "shirt", quantity: 2, price: 25 }); - const result = await checkout(cart, validPayment); - expect(result.status).toBe("confirmed"); - expect(result.total).toBe(50); -}); -``` - -This test: -- Uses the public API (`createCart`, `addItem`, `checkout`) -- Asserts on observable outcomes (`status`, `total`) -- Reads like a specification -- Would survive any internal refactor - -## Bad Tests - -Bad tests are coupled to implementation: - -```typescript -test("checkout calls payment gateway", async () => { - const mockGateway = jest.fn().mockResolvedValue({ ok: true }); - const cart = new Cart(); - cart._items = [{ id: "shirt", qty: 2 }]; // accessing internals - await cart._processCheckout(mockGateway); // testing private method - expect(mockGateway).toHaveBeenCalledWith(/* specific internal format */); -}); -``` - -Red flags: -- Accesses private/internal state (`_items`, `_processCheckout`) -- Mocks an internal collaborator -- Asserts on *how* it works, not *what* it produces -- Will break if internals change, even if behavior is identical From 8f18f1f2b0044f3891edc69e46d9264ac4d8527c Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 21 May 2026 16:56:08 -0700 Subject: [PATCH 08/23] revert to main --- src/components/plots/EquilibriumPlot.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/plots/EquilibriumPlot.tsx b/src/components/plots/EquilibriumPlot.tsx index bc3cfb9..5a0d81f 100644 --- a/src/components/plots/EquilibriumPlot.tsx +++ b/src/components/plots/EquilibriumPlot.tsx @@ -52,30 +52,31 @@ const EquilibriumPlot: React.FC = ({ xVal, y[index], ]); - let bestFit; + let fitResult; let value; if (module === Module.A_B_D_AB) { - bestFit = regression.exponential(regressionData); + fitResult = regression.exponential(regressionData); const max = Math.max(...y); const min = Math.min(...y); const halfMax = (max - min) / 2 + min; // for exponential, the equation is in the form y = a * e^(b*x) - // bestFit.equation[0] is a and bestFit.equation[1] is b, so to solve for x when y is halfMax: + // fitResult.equation[0] is a and fitResult.equation[1] is b, so to solve for x when y is halfMax: // halfMax = a * e^(b*x) // halfMax / a = e^(b*x) // ln(halfMax / a) = b*x // x = ln(halfMax / a) / b value = - Math.log(halfMax / bestFit.equation[0]) / bestFit.equation[1]; + Math.log(halfMax / fitResult.equation[0]) / + fitResult.equation[1]; } else { - bestFit = regression.logarithmic(regressionData); + fitResult = regression.logarithmic(regressionData); const halfFilled = fixedAgentStartingConcentration / 2; value = Math.E ** - ((halfFilled - bestFit.equation[0]) / bestFit.equation[1]); + ((halfFilled - fitResult.equation[0]) / fitResult.equation[1]); } - const bestFitPoints = bestFit.points; + const bestFitPoints = fitResult.points; const bestFitX = bestFitPoints.map((point) => point[0]); const bestFitY = bestFitPoints.map((point) => point[1]); From a7fb9e13aa1d40b6c1574c6f272a100325c948b8 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 21 May 2026 17:34:38 -0700 Subject: [PATCH 09/23] fix bug where plots were getting added with only 0,0 as a value --- src/App.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index db34ad7..307ab41 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -269,6 +269,9 @@ function App() { if (isLastFrame) { simulariumController.gotoTime(0); } + setCurrentProductConcentrationArray((prev) => + prev.length === 0 ? [0] : prev, + ); simulariumController.resume(); } else { simulariumController.pause(); @@ -608,7 +611,7 @@ function App() { ) as CurrentConcentration; } const productConcentration = concentrations[productName]; - if (productConcentration !== undefined) { + if (productConcentration !== undefined && isPlaying) { const newData = [...previousData, productConcentration]; setCurrentProductConcentrationArray(newData); } From 09488469a74827a548c4212965adc48621d11de5 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 21 May 2026 17:54:55 -0700 Subject: [PATCH 10/23] add a Ki question --- src/components/main-layout/CenterPanel.tsx | 10 +- src/components/quiz-questions/KiQuestion.tsx | 110 +++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/components/quiz-questions/KiQuestion.tsx diff --git a/src/components/main-layout/CenterPanel.tsx b/src/components/main-layout/CenterPanel.tsx index 22541d7..29851f8 100644 --- a/src/components/main-layout/CenterPanel.tsx +++ b/src/components/main-layout/CenterPanel.tsx @@ -2,7 +2,10 @@ 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; @@ -24,13 +27,18 @@ const CenterPanel: React.FC = ({ overlay, }) => { const [lastOpened, setLastOpened] = React.useState(null); + const { module } = useSimulariumUi(); return ( <>
- + {module === Module.A_B_D_AB ? ( + + ) : ( + + )}
{overlay && overlay} diff --git a/src/components/quiz-questions/KiQuestion.tsx b/src/components/quiz-questions/KiQuestion.tsx new file mode 100644 index 0000000..d2197c7 --- /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; From c905ea31f6642d3fee04453ca2cd927ee84181d3 Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Fri, 22 May 2026 10:42:03 -0700 Subject: [PATCH 11/23] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/components/plots/EquilibriumPlot.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/plots/EquilibriumPlot.tsx b/src/components/plots/EquilibriumPlot.tsx index 5a0d81f..b4f7a54 100644 --- a/src/components/plots/EquilibriumPlot.tsx +++ b/src/components/plots/EquilibriumPlot.tsx @@ -230,7 +230,7 @@ const EquilibriumPlot: React.FC = ({ color: getAgentColor(adjustableAgentName), }, tickmode: bestFitVisible ? ("array" as const) : ("auto" as const), - tickvals: [...xAxisTicks, bestFit.value.toFixed(1)], + tickvals: [...xAxisTicks, Number(bestFit.value.toFixed(1))], }, yaxis: { ...AXIS_SETTINGS, From db366eec3ae683dd31be619e01b98b34565683ce Mon Sep 17 00:00:00 2001 From: meganrm Date: Fri, 22 May 2026 10:44:46 -0700 Subject: [PATCH 12/23] change name of variable --- src/App.tsx | 8 ++++--- .../concentration-display/Concentration.tsx | 14 +++++++---- .../ConcentrationSlider.tsx | 23 ++++++++++++------- src/components/main-layout/CenterPanel.tsx | 8 +++---- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 307ab41..28bc976 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 && @@ -834,7 +834,9 @@ function App() { centerPanel={ } @@ -843,7 +845,7 @@ function App() { pageContent={{ ...pageContent, nextButton: - (canDetermineKd && + (canDetermineConstant && pageContent.section === Section.Experiment) || pageContent.nextButton, diff --git a/src/components/concentration-display/Concentration.tsx b/src/components/concentration-display/Concentration.tsx index 51d8491..f484565 100644 --- a/src/components/concentration-display/Concentration.tsx +++ b/src/components/concentration-display/Concentration.tsx @@ -7,10 +7,12 @@ import { AgentName, CurrentConcentration, InputConcentration, + Module, Section, UiElement, } from "../../types"; import { + useSimulariumAnalysis, useSimulariumSimulation, useSimulariumUi, } from "../../hooks/useSimulationContext"; @@ -47,7 +49,10 @@ const Concentration: React.FC = ({ }) => { 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); const [width, setWidth] = useState(0); const MARGINS = 64.2; @@ -79,13 +84,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..aea166e 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -7,12 +7,13 @@ import styles from "./concentration-slider.module.css"; import classNames from "classnames"; 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 +54,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,15 +77,20 @@ 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 ( = ({ kd, - canDetermineEquilibrium, + canDetermineConstant, overlay, }) => { const [lastOpened, setLastOpened] = React.useState(null); @@ -35,9 +35,9 @@ const CenterPanel: React.FC = ({
{module === Module.A_B_D_AB ? ( - + ) : ( - + )}
From b2a48881f8009bac4fa2aeae083f4fdfe80c1b01 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 26 May 2026 14:11:59 -0700 Subject: [PATCH 13/23] have a help popup when disabled --- bun.lockb | Bin 259146 -> 266970 bytes src/components/HelpPopup.tsx | 9 +++-- .../concentration-display/Concentration.tsx | 4 +- .../ConcentrationSlider.tsx | 35 +++++++++++------- src/components/main-layout/RightPanel.tsx | 2 +- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/bun.lockb b/bun.lockb index 9bd1e7a2a3235de28b300b91680b6d1e04fb9889..93780db4661ec956d77e15043e93433b3f818573 100755 GIT binary patch delta 27644 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 S@xOSnLCZxqw*0Af&Hn;28>;pI delta 23309 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 KvnsW0F8vP@@a~TQ 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 { module, section, progressionElement } = useSimulariumUi(); const { recordedConcentrations } = useSimulariumAnalysis(); const isSliderDisabled = - module === Module.A_B_D_AB && !recordedConcentrations.includes(0); + module === Module.A_B_D_AB && + !recordedConcentrations.includes(0) && + section === Section.Experiment; const [width, setWidth] = useState(0); const MARGINS = 64.2; diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx index aea166e..8fca29e 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -5,6 +5,7 @@ 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 { disabled?: boolean; @@ -89,19 +90,27 @@ const ConcentrationSlider: React.FC = ({ return marks; }, [min, max, disabledNumbers, disabled, onChangeComplete, name, stepSize]); return ( - + + + + + ); }; diff --git a/src/components/main-layout/RightPanel.tsx b/src/components/main-layout/RightPanel.tsx index aaed196..684c670 100644 --- a/src/components/main-layout/RightPanel.tsx +++ b/src/components/main-layout/RightPanel.tsx @@ -71,7 +71,7 @@ const RightPanel: React.FC = ({ content={ "Use this plot to help determine when the reaction reaches equilibrium." } - initialOpen={showHelpPanel} + open={showHelpPanel} > Date: Tue, 26 May 2026 14:12:46 -0700 Subject: [PATCH 14/23] use satisfies --- src/simulation/context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/simulation/context.tsx b/src/simulation/context.tsx index f4c18b5..200f115 100644 --- a/src/simulation/context.tsx +++ b/src/simulation/context.tsx @@ -66,7 +66,7 @@ export const SimulariumUiContext = createContext({ setPage: () => {}, setViewportType: () => {}, viewportType: ViewType.Lab, -} as SimulariumUiContextType); +} satisfies SimulariumUiContextType); export const SimulariumSimulationContext = createContext({ adjustableAgentName: AgentName.B, From a1f8309ed5fd2356c50e6924bbb14af49c2fe9dd Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 26 May 2026 14:13:34 -0700 Subject: [PATCH 15/23] move into docs folder --- CONTEXT_ORGANIZATION.md => docs/CONTEXT_ORGANIZATION.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CONTEXT_ORGANIZATION.md => docs/CONTEXT_ORGANIZATION.md (100%) diff --git a/CONTEXT_ORGANIZATION.md b/docs/CONTEXT_ORGANIZATION.md similarity index 100% rename from CONTEXT_ORGANIZATION.md rename to docs/CONTEXT_ORGANIZATION.md From 46ed355fdd5d209796737c363fa679185dfa8886 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 26 May 2026 14:14:30 -0700 Subject: [PATCH 16/23] remove personal notification setting --- CLAUDE.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index db8f0f1..8c6d234 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,11 +40,11 @@ import { useSimulariumUi, useSimulariumSimulation, useSimulariumAnalysis } from ## Build Commands -| What | Command | -|------|---------| -| Dev server | `bun run dev` | -| Build | `bun run build` | -| Lint | `bun run lint` | +| What | Command | +| ---------- | ------------------- | +| Dev server | `bun run dev` | +| Build | `bun run build` | +| Lint | `bun run lint` | | Type check | `bunx tsc --noEmit` | --- @@ -69,8 +69,3 @@ import { useSimulariumUi, useSimulariumSimulation, useSimulariumAnalysis } from ### Import order External libs → internal aliases → relative imports, each group alphabetical. ---- - -## Notification - -After finishing work, run: `afplay /System/Library/Sounds/Funk.aiff` From 0a92a61075b53841e8938842dae385c2e55d76c6 Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 26 May 2026 14:15:30 -0700 Subject: [PATCH 17/23] remove whitespace --- src/components/main-layout/LeftPanel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/main-layout/LeftPanel.tsx b/src/components/main-layout/LeftPanel.tsx index 5314a9f..acc3ed9 100644 --- a/src/components/main-layout/LeftPanel.tsx +++ b/src/components/main-layout/LeftPanel.tsx @@ -31,7 +31,7 @@ const LeftPanel: React.FC = ({ adjustableAgent, }) => { const { module } = useSimulariumUi(); - + const concentrationExcludedPages = { [Module.A_B_AB]: [0, 1], [Module.A_C_AC]: [], @@ -42,10 +42,10 @@ const LeftPanel: React.FC = ({ [Module.A_B_AB]: [0, 1, 2], [Module.A_C_AC]: [], }; - + // Don't show events over time plot in the competitive binding module const showEventsOverTime = module !== Module.A_B_D_AB; - + return ( <> From d7736e603f307ee1cac4f033ebad32a8c248f73e Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 26 May 2026 14:42:23 -0700 Subject: [PATCH 18/23] use createContext --- src/simulation/context.tsx | 54 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/simulation/context.tsx b/src/simulation/context.tsx index 200f115..c5e7c8e 100644 --- a/src/simulation/context.tsx +++ b/src/simulation/context.tsx @@ -53,7 +53,7 @@ export interface SimulariumAnalysisContextType { resetAnalysisState: () => void; } -export const SimulariumUiContext = createContext({ +export const SimulariumUiContext = createContext({ addCompletedModule: () => {}, completedModules: new Set(), module: Module.A_B_AB, @@ -66,30 +66,32 @@ export const SimulariumUiContext = createContext({ setPage: () => {}, setViewportType: () => {}, viewportType: ViewType.Lab, -} satisfies SimulariumUiContextType); +}); -export const SimulariumSimulationContext = createContext({ - adjustableAgentName: AgentName.B, - currentProductionConcentration: 0, - fixedAgentStartingConcentration: 0, - getAgentColor: () => "", - handleMixAgents: () => {}, - handleStartExperiment: () => {}, - handleTimeChange: () => {}, - handleTrajectoryChange: () => {}, - isPlaying: false, - maxConcentration: 10, - productName: ProductName.AB, - setIsPlaying: () => {}, - setViewportSize: () => {}, - simulariumController: null, - timeFactor: 30, - timeUnit: NANO, - trajectoryName: LIVE_SIMULATION_NAME, - viewportSize: DEFAULT_VIEWPORT_SIZE, -} as SimulariumSimulationContextType); +export const SimulariumSimulationContext = + createContext({ + adjustableAgentName: AgentName.B, + currentProductionConcentration: 0, + fixedAgentStartingConcentration: 0, + getAgentColor: () => "", + handleMixAgents: () => {}, + handleStartExperiment: () => {}, + handleTimeChange: () => {}, + handleTrajectoryChange: () => {}, + isPlaying: false, + maxConcentration: 10, + productName: ProductName.AB, + setIsPlaying: () => {}, + setViewportSize: () => {}, + simulariumController: null, + timeFactor: 30, + timeUnit: NANO, + trajectoryName: LIVE_SIMULATION_NAME, + viewportSize: DEFAULT_VIEWPORT_SIZE, + }); -export const SimulariumAnalysisContext = createContext({ - recordedConcentrations: [], - resetAnalysisState: () => {}, -} as SimulariumAnalysisContextType); +export const SimulariumAnalysisContext = + createContext({ + recordedConcentrations: [], + resetAnalysisState: () => {}, + }); From 5bc7c9ee43d1effa201db6c65d336bd1df755afd Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Tue, 26 May 2026 16:26:27 -0700 Subject: [PATCH 19/23] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/components/quiz-questions/KiQuestion.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/quiz-questions/KiQuestion.tsx b/src/components/quiz-questions/KiQuestion.tsx index d2197c7..d5fb5a4 100644 --- a/src/components/quiz-questions/KiQuestion.tsx +++ b/src/components/quiz-questions/KiQuestion.tsx @@ -68,7 +68,7 @@ const KiQuestion: React.FC = ({ canAnswer, ki }) => { const formContent = (
-

+

You have now measured enough points to estimate the concentration of D where inhibition reduces binding by 50% (IC₅₀). From 5f605df8faf6b2e2bd15f1d1b001a94da8cfa92d Mon Sep 17 00:00:00 2001 From: meganrm Date: Wed, 27 May 2026 15:33:41 -0700 Subject: [PATCH 20/23] make name more generic --- src/App.tsx | 11 +++++++---- src/components/main-layout/CenterPanel.tsx | 14 ++++++++++---- src/components/main-layout/RightPanel.tsx | 4 ++-- src/components/plots/EquilibriumPlot.tsx | 6 +++--- src/components/quiz-questions/KiQuestion.tsx | 2 +- src/simulation/LiveSimulationData.ts | 2 +- src/simulation/PreComputedSimulationData.ts | 2 +- 7 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 28bc976..bd5e962 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -833,7 +833,9 @@ function App() { = ({ - kd, + eqConstant, canDetermineConstant, overlay, }) => { @@ -35,9 +35,15 @@ const CenterPanel: React.FC = ({

{module === Module.A_B_D_AB ? ( - + ) : ( - + )}
diff --git a/src/components/main-layout/RightPanel.tsx b/src/components/main-layout/RightPanel.tsx index 684c670..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; @@ -94,7 +94,7 @@ const RightPanel: React.FC = ({ 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 index d5fb5a4..00e2798 100644 --- a/src/components/quiz-questions/KiQuestion.tsx +++ b/src/components/quiz-questions/KiQuestion.tsx @@ -82,7 +82,7 @@ const KiQuestion: React.FC = ({ canAnswer, ki }) => { { + 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; }; } From fdfd99579e80f87a8791dcc4412f6c00c3d2817e Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 28 May 2026 10:07:49 -0700 Subject: [PATCH 21/23] change to div --- src/components/concentration-display/ConcentrationSlider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx index 8fca29e..3dfc036 100644 --- a/src/components/concentration-display/ConcentrationSlider.tsx +++ b/src/components/concentration-display/ConcentrationSlider.tsx @@ -95,7 +95,7 @@ const ConcentrationSlider: React.FC = ({ trigger="hover" open={disabled ? undefined : false} > - +
= ({ marks={marks} disabledNumbers={disabledNumbers} /> - +
); }; From 982a987b00d93859d0e4f8b5de3e145f05391885 Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Wed, 3 Jun 2026 13:38:15 -0700 Subject: [PATCH 22/23] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8c6d234..cd265d5 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) From b4443b96af09ac1481aa9f196cf69db64501aa38 Mon Sep 17 00:00:00 2001 From: Megan Riel-Mehan Date: Wed, 3 Jun 2026 13:39:04 -0700 Subject: [PATCH 23/23] doc moved Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CLAUDE.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cd265d5..8049fa4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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