Skip to content

Commit 5402643

Browse files
amirhkclaude
andcommitted
fix: live costs for all actions, smart constraint-aware deselection
- All three action buttons now show a live cost badge that updates whenever the boundary type, robustness δ, or constraint set changes — no need to select an action first to see its cost change - Constraint toggles are now smart: a selected action is only cleared when the new constraint actually blocks it (e.g. selecting a single-feature action then enabling sparsity keeps the selection; enabling non-actionable only clears if the selected action touches the savings feature; robustness and δ never clear any selection) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f062675 commit 5402643

1 file changed

Lines changed: 44 additions & 19 deletions

File tree

src/components/InteractiveDemo.jsx

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -195,25 +195,34 @@ export default function InteractiveDemo() {
195195
const deltaRingR = delta * PH * 0.48;
196196
const BASE_MARGIN = 5; // px margin when robustness off
197197

198-
function isDisabled(action) {
199-
if (nonActionable && (action.feature === 'savings' || action.feature === 'both')) return true;
200-
if (sparsity && action.feature === 'both') return true;
198+
function isDisabled(action, na = nonActionable, sp = sparsity) {
199+
if (na && (action.feature === 'savings' || action.feature === 'both')) return true;
200+
if (sp && action.feature === 'both') return true;
201201
return false;
202202
}
203203

204-
// Compute current endpoint + cost for the selected action
205-
const { endpoint: selEndpoint, cost: selCost } = useMemo(() => {
206-
const action = ACTIONS.find(a => a.id === selectedAction);
207-
if (!action || isDisabled(action)) return { endpoint: null, cost: 0 };
204+
// Compute live endpoints + costs for ALL actions whenever anything relevant changes.
205+
// This means every action badge shows an up-to-date cost reflecting the current
206+
// boundary shape, robustness level, and constraint set.
207+
const allEndpoints = useMemo(() => {
208208
const targetDist = robustness ? deltaRingR : BASE_MARGIN;
209-
const ep = computeEndpoint(selectedAction, boundaryType, targetDist);
210-
// Scale cost proportionally to how far the endpoint moved vs. reference
211-
const baseDist = Math.hypot(action.baseTo[0] - 25, action.baseTo[1] - 30);
212-
const epDist = Math.hypot(ep[0] - 25, ep[1] - 30);
213-
const cost = +(action.cost * Math.max(1, epDist / Math.max(baseDist, 1))).toFixed(1);
214-
return { endpoint: ep, cost };
209+
const result = {};
210+
for (const action of ACTIONS) {
211+
if (isDisabled(action)) continue;
212+
const ep = computeEndpoint(action.id, boundaryType, targetDist);
213+
const baseDist = Math.hypot(action.baseTo[0] - 25, action.baseTo[1] - 30);
214+
const epDist = Math.hypot(ep[0] - 25, ep[1] - 30);
215+
result[action.id] = {
216+
ep,
217+
cost: +(action.cost * Math.max(1, epDist / Math.max(baseDist, 1))).toFixed(1),
218+
};
219+
}
220+
return result;
215221
// eslint-disable-next-line react-hooks/exhaustive-deps
216-
}, [selectedAction, boundaryType, robustness, deltaRingR]);
222+
}, [boundaryType, robustness, deltaRingR, nonActionable, sparsity]);
223+
224+
const selEndpoint = allEndpoints[selectedAction]?.ep ?? null;
225+
const selCost = allEndpoints[selectedAction]?.cost ?? 0;
217226

218227
const youPt = toSvg(25, 30);
219228
const formulation = buildFormulation(nonActionable, robustness, sparsity, delta);
@@ -396,8 +405,8 @@ export default function InteractiveDemo() {
396405
{ACTIONS.map(action => {
397406
const disabled = isDisabled(action);
398407
const active = selectedAction === action.id && !disabled;
399-
// Show dynamic cost when this action is active, else base cost
400-
const displayCost = active ? selCost : action.cost;
408+
// All buttons show live cost — updates with model type, δ, and constraints
409+
const liveCost = allEndpoints[action.id]?.cost ?? action.cost;
401410
return (
402411
<button key={action.id} type="button" disabled={disabled}
403412
className={`demo-action${active ? ' active' : ''}${disabled ? ' disabled' : ''}`}
@@ -407,7 +416,7 @@ export default function InteractiveDemo() {
407416
<span className="demo-action-label">{action.label}</span>
408417
<span className="demo-action-cost"
409418
style={{ background: action.soft, color: action.color }}>
410-
{disabled ? 'blocked' : `cost ${displayCost}`}
419+
{disabled ? 'blocked' : `cost ${liveCost}`}
411420
</span>
412421
</button>
413422
);
@@ -421,15 +430,31 @@ export default function InteractiveDemo() {
421430
<div className="demo-toggle-list">
422431
<label className="demo-toggle">
423432
<input type="checkbox" checked={nonActionable}
424-
onChange={e => { setNonActionable(e.target.checked); setSelectedAction(null); }}/>
433+
onChange={e => {
434+
const checked = e.target.checked;
435+
setNonActionable(checked);
436+
// Only deselect if the active action becomes blocked by this constraint
437+
if (checked && selectedAction) {
438+
const sel = ACTIONS.find(a => a.id === selectedAction);
439+
if (sel && isDisabled(sel, checked, sparsity)) setSelectedAction(null);
440+
}
441+
}}/>
425442
<span className="demo-toggle-track"/>
426443
<span className="demo-toggle-text">
427444
Non-actionable: <em>Savings Rate</em>
428445
</span>
429446
</label>
430447
<label className="demo-toggle">
431448
<input type="checkbox" checked={sparsity}
432-
onChange={e => { setSparsity(e.target.checked); setSelectedAction(null); }}/>
449+
onChange={e => {
450+
const checked = e.target.checked;
451+
setSparsity(checked);
452+
// Only deselect if the active action becomes blocked by this constraint
453+
if (checked && selectedAction) {
454+
const sel = ACTIONS.find(a => a.id === selectedAction);
455+
if (sel && isDisabled(sel, nonActionable, checked)) setSelectedAction(null);
456+
}
457+
}}/>
433458
<span className="demo-toggle-track"/>
434459
<span className="demo-toggle-text">Sparsity <em>k = 1</em> (one feature only)</span>
435460
</label>

0 commit comments

Comments
 (0)