CarlyneB commited on
Commit
84fd744
·
1 Parent(s): c0da4ca

[FEAT/FIX] tab1: add load/download standard resources buttons, fix stale costs on edit/delete/reset, increase number precision

Browse files
Files changed (2) hide show
  1. assets/styles.css +7 -3
  2. tabs/tab1_inputs.py +163 -51
assets/styles.css CHANGED
@@ -242,6 +242,7 @@ h1.custom-title {
242
  .st-key-btn-reset button p,
243
  [class*="btn-reset-"] button p,
244
  .st-key-btn-load button p,
 
245
  [class*="btn-new-"] button p,
246
  [class*="st-key-download_button_"] button p,
247
  [data-testid="stDownloadButton"] button p,
@@ -276,7 +277,8 @@ h1.custom-title {
276
  }
277
 
278
  /* Load — green-dark */
279
- .st-key-btn-load button {
 
280
  border: none !important;
281
  border-radius: 0px;
282
  color: var(--off-white);
@@ -286,7 +288,8 @@ h1.custom-title {
286
  clip-path: var(--btn-clip);
287
  transition: background 0.25s ease;
288
  }
289
- .st-key-btn-load button:hover {
 
290
  background: #01740f;
291
  color: var(--off-white);
292
  }
@@ -573,7 +576,8 @@ section[data-testid="stExpander"] summary p {
573
  }
574
 
575
  /* ── 16. Button container layout ────────────────────────────── */
576
- .st-key-container-btn{
 
577
  display: flex;
578
  flex-direction: row;
579
  gap: 1.125%;
 
242
  .st-key-btn-reset button p,
243
  [class*="btn-reset-"] button p,
244
  .st-key-btn-load button p,
245
+ .st-key-btn-load-resources button p,
246
  [class*="btn-new-"] button p,
247
  [class*="st-key-download_button_"] button p,
248
  [data-testid="stDownloadButton"] button p,
 
277
  }
278
 
279
  /* Load — green-dark */
280
+ .st-key-btn-load button,
281
+ .st-key-btn-load-resources button {
282
  border: none !important;
283
  border-radius: 0px;
284
  color: var(--off-white);
 
288
  clip-path: var(--btn-clip);
289
  transition: background 0.25s ease;
290
  }
291
+ .st-key-btn-load button:hover,
292
+ .st-key-btn-load-resources button:hover {
293
  background: #01740f;
294
  color: var(--off-white);
295
  }
 
576
  }
577
 
578
  /* ── 16. Button container layout ────────────────────────────── */
579
+ .st-key-container-btn,
580
+ .st-key-container-btn-resources{
581
  display: flex;
582
  flex-direction: row;
583
  gap: 1.125%;
tabs/tab1_inputs.py CHANGED
@@ -5,7 +5,8 @@ from config.config import (resource_groups, resource_order, resource_tooltips,
5
  resource_unit_map, method_groups, CO2_UNIT, secondary_resources,
6
  hidden_resource_names)
7
  from utils.data_utils import (strip_unit_suffix, get_resource_to_group, get_resource_to_unit,
8
- get_method_group, standard_resource_names, to_excel_bytes, parse_custom_excel)
 
9
  from utils.state_utils import apply_auto_disable_logic, init_custom_resources
10
  from utils.ui_helpers import show_bar, render_resource_input
11
 
@@ -111,7 +112,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
111
  batch_amount = st.number_input(
112
  label=f"Available amount ({unit}/yr)",
113
  min_value=0.0,
114
- format="%.2f",
115
  value=float(existing["amount"]) if existing.get("amount") and existing.get("amount") != 0.0 else None,
116
  help=f"Total quantity available for 1 year, in {unit}.",
117
  key=f"label-input-amount-{eix}",
@@ -222,17 +223,20 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
222
  continue
223
  with st.expander(f"**{group_name}**", expanded=False):
224
  for m in available_methods:
 
225
  enabled = st.checkbox(
226
  f"**{m}**",
 
227
  key=f"_dlg_new_chk_{eix}_{m}",
228
  )
229
  if enabled:
 
230
  col_min, col_med, col_max = st.columns(3)
231
  with col_min:
232
  v_min = st.number_input(
233
  f"Min ({unit}/{CO2_UNIT})",
234
  min_value=0.0,
235
- value=None,
236
  format="%.10g",
237
  key=f"_dlg_new_min_{eix}_{m}",
238
  placeholder="0.0000",
@@ -241,7 +245,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
241
  v_med = st.number_input(
242
  f"Median ({unit}/{CO2_UNIT})",
243
  min_value=0.0,
244
- value=None,
245
  format="%.10g",
246
  key=f"_dlg_new_med_{eix}_{m}",
247
  placeholder="0.0000",
@@ -250,7 +254,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
250
  v_max = st.number_input(
251
  f"Max ({unit}/{CO2_UNIT})",
252
  min_value=0.0,
253
- value=None,
254
  format="%.10g",
255
  key=f"_dlg_new_max_{eix}_{m}",
256
  placeholder="0.0000",
@@ -273,7 +277,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
273
  errors.append("Resource name is required.")
274
  elif batch_name.strip().lower() in {n.lower() for n in standard_names}:
275
  errors.append(f"**{batch_name}** matches a standard resource name. Choose a different name.")
276
- elif batch_name.strip() in existing_names:
277
  errors.append(f"**{batch_name}** already exists. Choose a different name.")
278
  if not batch_amount or batch_amount <= 0:
279
  errors.append("Available amount must be greater than 0.")
@@ -305,12 +309,55 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
305
  "methods": method_configs,
306
  }
307
  if edit_index is not None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  st.session_state.custom_resources[edit_index] = batch
309
  st.session_state.pop(f"custom_amount_{edit_index}", None)
310
  else:
311
  new_idx = len(st.session_state.custom_resources)
312
  st.session_state.custom_resources.append(batch)
313
  st.session_state.pop(f"custom_amount_{new_idx}", None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  st.session_state.pop("latest_result", None)
315
  st.rerun()
316
 
@@ -367,6 +414,13 @@ def dialog_load_custom_batch():
367
  if batch["name"] in existing_names:
368
  idx = next(i for i, b in enumerate(st.session_state.custom_resources)
369
  if b["name"] == batch["name"])
 
 
 
 
 
 
 
370
  st.session_state.custom_resources[idx] = batch
371
  else:
372
  st.session_state.custom_resources.append(batch)
@@ -376,6 +430,65 @@ def dialog_load_custom_batch():
376
  st.rerun()
377
 
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
  def render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
381
  """Render a single custom batch inside its resource-group expander."""
@@ -392,7 +505,7 @@ def render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
392
  new_amount = st.number_input(
393
  f"{batch['name']}",
394
  min_value=0.0,
395
- format="%.2f",
396
  key=key_amount,
397
  placeholder="0.0",
398
  )
@@ -421,9 +534,9 @@ def render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
421
  key=f"batch_editor_{global_idx}",
422
  column_config={
423
  "Method": st.column_config.TextColumn(disabled=True),
424
- col_min: st.column_config.NumberColumn(format="%.4f", min_value=0.0),
425
- col_med: st.column_config.NumberColumn(format="%.4f", min_value=0.0),
426
- col_max: st.column_config.NumberColumn(format="%.4f", min_value=0.0),
427
  },
428
  )
429
  else:
@@ -444,7 +557,14 @@ def render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
444
  with st.container(key=f"container-button-{global_idx}"):
445
  if st.button("Delete", key=f"del_batch_{global_idx}",
446
  type="primary", icon=":material/delete:"):
447
- st.session_state.custom_resources.pop(global_idx)
 
 
 
 
 
 
 
448
  st.session_state.pop("latest_result", None)
449
  st.rerun()
450
  if st.button("Edit", key=f"edit_batch_{global_idx}",
@@ -648,17 +768,25 @@ def render_tab(cost_sliders):
648
  if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
649
  for r in st.session_state.resource_caps:
650
  st.session_state.resource_caps[r] = 0.0
 
 
 
 
 
 
651
  st.session_state.custom_resources = []
652
  st.session_state.enabled_methods = {}
653
  st.success("All resource values reset to 0.")
654
  st.rerun()
655
 
656
  st.markdown('<p class="crra-sub-sub-heading">Import resource values</p>', unsafe_allow_html=True)
657
- col_template, col_upload = st.columns([1, 1])
658
- with col_template:
659
- st.markdown("<div class='caption'><p>Use the template below to fill in your resource "
660
- "availability values and import them directly into the tool.</p></div>",
661
- unsafe_allow_html=True)
 
 
662
  with open("data/resource_template.xlsx", "rb") as f:
663
  st.download_button(
664
  label="Download Excel Template",
@@ -668,41 +796,10 @@ def render_tab(cost_sliders):
668
  icon=":material/download:",
669
  key="download_button_resource_template"
670
  )
671
- with col_upload:
672
- uploaded_file = st.file_uploader(
673
- "Upload your Excel file", type="xlsx",
674
- key=f"resource_uploader_{st.session_state.get('resource_uploader_key', 0)}"
675
- )
676
- if uploaded_file is not None and uploaded_file.name != st.session_state.get("resource_last_loaded_file"):
677
- try:
678
- df = pd.read_excel(uploaded_file)
679
- required_cols = {"Resource", "Available Amount"}
680
- if not required_cols.issubset(df.columns):
681
- raise ValueError(f"Missing columns: {required_cols - set(df.columns)}")
682
- for r in st.session_state.resource_caps:
683
- st.session_state.resource_caps[r] = 0.0
684
- st.session_state[f"cap_{r}"] = 0.0
685
- for _, row in df.iterrows():
686
- r = row["Resource"]
687
- val = row["Available Amount"]
688
- if r not in st.session_state.resource_caps:
689
- continue
690
- try:
691
- val = 0.0 if pd.isna(val) else float(val)
692
- except (ValueError, TypeError):
693
- val = 0.0
694
- st.session_state.resource_caps[r] = val
695
- st.session_state[f"cap_{r}"] = val
696
- st.session_state["resource_last_loaded_file"] = uploaded_file.name
697
- st.success("Resource values imported successfully.", icon="✅")
698
- for key in ["excel_loaded_file", "scenario_name_suggestion",
699
- "scenario_loaded_file", "input_file_name_suggestion", "latest_result"]:
700
- st.session_state.pop(key, None)
701
- st.rerun()
702
- except Exception as e:
703
- st.error(f"Error importing resource values: {e}")
704
- elif st.session_state.get("resource_last_loaded_file"):
705
- st.success(f"Loaded: **{st.session_state['resource_last_loaded_file']}**", icon="✅")
706
 
707
  # Standard resource inputs
708
  st.markdown('<p class="crra-sub-heading">Set available resources for one year</p>', unsafe_allow_html=True)
@@ -766,6 +863,21 @@ def render_tab(cost_sliders):
766
 
767
  apply_auto_disable_logic(method_to_primary_resources, secondary_resources)
768
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
769
  panel_custom_resources_controls(cost_sliders)
770
 
771
  render_resource_summary()
 
5
  resource_unit_map, method_groups, CO2_UNIT, secondary_resources,
6
  hidden_resource_names)
7
  from utils.data_utils import (strip_unit_suffix, get_resource_to_group, get_resource_to_unit,
8
+ get_method_group, standard_resource_names, to_excel_bytes, parse_custom_excel,
9
+ resource_caps_to_excel_bytes)
10
  from utils.state_utils import apply_auto_disable_logic, init_custom_resources
11
  from utils.ui_helpers import show_bar, render_resource_input
12
 
 
112
  batch_amount = st.number_input(
113
  label=f"Available amount ({unit}/yr)",
114
  min_value=0.0,
115
+ format="%.10g",
116
  value=float(existing["amount"]) if existing.get("amount") and existing.get("amount") != 0.0 else None,
117
  help=f"Total quantity available for 1 year, in {unit}.",
118
  key=f"label-input-amount-{eix}",
 
223
  continue
224
  with st.expander(f"**{group_name}**", expanded=False):
225
  for m in available_methods:
226
+ already_selected = m in existing.get("methods", {})
227
  enabled = st.checkbox(
228
  f"**{m}**",
229
+ value=already_selected,
230
  key=f"_dlg_new_chk_{eix}_{m}",
231
  )
232
  if enabled:
233
+ existing_coeff = existing.get("methods", {}).get(m, {})
234
  col_min, col_med, col_max = st.columns(3)
235
  with col_min:
236
  v_min = st.number_input(
237
  f"Min ({unit}/{CO2_UNIT})",
238
  min_value=0.0,
239
+ value=float(existing_coeff["min"]) if existing_coeff.get("min") is not None else None,
240
  format="%.10g",
241
  key=f"_dlg_new_min_{eix}_{m}",
242
  placeholder="0.0000",
 
245
  v_med = st.number_input(
246
  f"Median ({unit}/{CO2_UNIT})",
247
  min_value=0.0,
248
+ value=float(existing_coeff["median"]) if existing_coeff.get("median") is not None else None,
249
  format="%.10g",
250
  key=f"_dlg_new_med_{eix}_{m}",
251
  placeholder="0.0000",
 
254
  v_max = st.number_input(
255
  f"Max ({unit}/{CO2_UNIT})",
256
  min_value=0.0,
257
+ value=float(existing_coeff["max"]) if existing_coeff.get("max") is not None else None,
258
  format="%.10g",
259
  key=f"_dlg_new_max_{eix}_{m}",
260
  placeholder="0.0000",
 
277
  errors.append("Resource name is required.")
278
  elif batch_name.strip().lower() in {n.lower() for n in standard_names}:
279
  errors.append(f"**{batch_name}** matches a standard resource name. Choose a different name.")
280
+ elif batch_name.strip().lower() in {n.lower() for n in existing_names}:
281
  errors.append(f"**{batch_name}** already exists. Choose a different name.")
282
  if not batch_amount or batch_amount <= 0:
283
  errors.append("Available amount must be greater than 0.")
 
309
  "methods": method_configs,
310
  }
311
  if edit_index is not None:
312
+ # Clean up stale costs[method][old_name] entries left behind by
313
+ # this edit — otherwise tab4's missing-resource check wrongly
314
+ # deactivates that method on the next optimization run. Two
315
+ # triggers: (a) a method that was previously assigned is no
316
+ # longer checked, or (b) the batch itself was renamed, which
317
+ # orphans the old name even for methods still checked (the new
318
+ # name gets a fresh entry injected at run time, but the old
319
+ # name's entry is never removed on its own).
320
+ old_name = existing.get("name", "")
321
+ new_name = batch_name.strip()
322
+ renamed = bool(old_name) and old_name != new_name
323
+ for method_name in existing.get("methods", {}):
324
+ if renamed or method_name not in method_configs:
325
+ st.session_state.costs.get(method_name, {}).pop(old_name, None)
326
  st.session_state.custom_resources[edit_index] = batch
327
  st.session_state.pop(f"custom_amount_{edit_index}", None)
328
  else:
329
  new_idx = len(st.session_state.custom_resources)
330
  st.session_state.custom_resources.append(batch)
331
  st.session_state.pop(f"custom_amount_{new_idx}", None)
332
+ # Push the new median into costs for any (method, coefficient)
333
+ # pair that actually changed in this edit — tab3's own init only
334
+ # sets a value the first time a key appears (`if r not in
335
+ # costs[m]`), so without this a coefficient edit made here would
336
+ # never overwrite the stale value already there. Only touch
337
+ # pairs whose median actually changed (or that are brand new) so
338
+ # a deliberate Tab 3 slider adjustment isn't silently reset just
339
+ # because the user edited something unrelated (e.g. the amount).
340
+ old_methods = existing.get("methods", {})
341
+ for method_name, coeff in method_configs.items():
342
+ old_coeff = old_methods.get(method_name, {})
343
+ if old_coeff.get("median") != coeff["median"]:
344
+ st.session_state.costs.setdefault(method_name, {})[batch["name"]] = coeff["median"]
345
+ # If min/max changed, tab3's current scenario value (used to
346
+ # seed its slider/manual widgets, keyed only by method+resource
347
+ # name so it persists across this edit) may now fall outside
348
+ # the new range — st.slider raises if its stored value is out
349
+ # of [min_value, max_value]. Clamp it into the new bounds
350
+ # (rather than resetting to the median) so the user's chosen
351
+ # operating point is kept as close as possible, then clear the
352
+ # widget keys so tab3 re-initialises from this clamped value.
353
+ if old_coeff.get("min") != coeff["min"] or old_coeff.get("max") != coeff["max"]:
354
+ current = st.session_state.costs.get(method_name, {}).get(batch["name"])
355
+ if current is not None:
356
+ clamped = max(coeff["min"], min(coeff["max"], current))
357
+ st.session_state.costs.setdefault(method_name, {})[batch["name"]] = clamped
358
+ key_suffix = f"{method_name}_{batch['name']}".replace(" ", "_")
359
+ for prefix in ("slider_", "manual_", "toggle_"):
360
+ st.session_state.pop(f"{prefix}{key_suffix}", None)
361
  st.session_state.pop("latest_result", None)
362
  st.rerun()
363
 
 
414
  if batch["name"] in existing_names:
415
  idx = next(i for i, b in enumerate(st.session_state.custom_resources)
416
  if b["name"] == batch["name"])
417
+ # Purge costs[method][batch_name] for methods dropped by the
418
+ # re-imported version — harmless today (build_lp_indices gates
419
+ # on the batch's own primary_methods) but avoids stale data
420
+ # lingering in session state until the next Tab 3 visit.
421
+ old_methods = set(st.session_state.custom_resources[idx].get("methods", {}))
422
+ for method_name in old_methods - set(batch.get("methods", {})):
423
+ st.session_state.costs.get(method_name, {}).pop(batch["name"], None)
424
  st.session_state.custom_resources[idx] = batch
425
  else:
426
  st.session_state.custom_resources.append(batch)
 
430
  st.rerun()
431
 
432
 
433
+ @st.dialog("Load standard resource values", width="large")
434
+ def dialog_load_standard_resources():
435
+ """Dialog for importing standard resource availabilities from an Excel file.
436
+
437
+ Reads an .xlsx file with "Resource" and "Available Amount" columns (the
438
+ same format as the downloadable template / current-values export) and
439
+ overwrites st.session_state.resource_caps on confirmation.
440
+
441
+ Returns:
442
+ None
443
+ """
444
+ st.markdown(
445
+ '<p class="caption">Upload an Excel file with "Resource" and "Available Amount" '
446
+ "columns — either the blank template or a previous export of your current values.</p>",
447
+ unsafe_allow_html=True,
448
+ )
449
+
450
+ uploaded = st.file_uploader("Upload Excel file", type=["xlsx"], key="resource_loader_dialog")
451
+ if uploaded is None:
452
+ return
453
+
454
+ try:
455
+ df = pd.read_excel(uploaded)
456
+ required_cols = {"Resource", "Available Amount"}
457
+ if not required_cols.issubset(df.columns):
458
+ raise ValueError(f"Missing columns: {required_cols - set(df.columns)}")
459
+ except Exception as e:
460
+ st.error(f"Error reading file: {e}")
461
+ return
462
+
463
+ preview_rows = [
464
+ {"Resource": row["Resource"], "Available Amount": row["Available Amount"]}
465
+ for _, row in df.iterrows()
466
+ if row["Resource"] in st.session_state.resource_caps
467
+ ]
468
+ st.dataframe(pd.DataFrame(preview_rows), width='stretch', hide_index=True)
469
+
470
+ if st.button("↩️ Import values", type="primary", width='stretch'):
471
+ for r in st.session_state.resource_caps:
472
+ st.session_state.resource_caps[r] = 0.0
473
+ st.session_state[f"cap_{r}"] = 0.0
474
+ for _, row in df.iterrows():
475
+ r = row["Resource"]
476
+ if r not in st.session_state.resource_caps:
477
+ continue
478
+ val = row["Available Amount"]
479
+ try:
480
+ val = 0.0 if pd.isna(val) else float(val)
481
+ except (ValueError, TypeError):
482
+ val = 0.0
483
+ st.session_state.resource_caps[r] = val
484
+ st.session_state[f"cap_{r}"] = val
485
+ st.session_state["resource_last_loaded_file"] = uploaded.name
486
+ for key in ["excel_loaded_file", "scenario_name_suggestion",
487
+ "scenario_loaded_file", "input_file_name_suggestion", "latest_result"]:
488
+ st.session_state.pop(key, None)
489
+ st.success("Resource values imported successfully.", icon="✅")
490
+ st.rerun()
491
+
492
 
493
  def render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
494
  """Render a single custom batch inside its resource-group expander."""
 
505
  new_amount = st.number_input(
506
  f"{batch['name']}",
507
  min_value=0.0,
508
+ format="%.10g",
509
  key=key_amount,
510
  placeholder="0.0",
511
  )
 
534
  key=f"batch_editor_{global_idx}",
535
  column_config={
536
  "Method": st.column_config.TextColumn(disabled=True),
537
+ col_min: st.column_config.NumberColumn(format="%.10g", min_value=0.0),
538
+ col_med: st.column_config.NumberColumn(format="%.10g", min_value=0.0),
539
+ col_max: st.column_config.NumberColumn(format="%.10g", min_value=0.0),
540
  },
541
  )
542
  else:
 
557
  with st.container(key=f"container-button-{global_idx}"):
558
  if st.button("Delete", key=f"del_batch_{global_idx}",
559
  type="primary", icon=":material/delete:"):
560
+ deleted = st.session_state.custom_resources.pop(global_idx)
561
+ # Clean up any coefficient this batch left in st.session_state.costs
562
+ # for the methods it was assigned to. Without this, a stale
563
+ # costs[method][batch_name] entry survives the deletion, and tab4's
564
+ # "missing resource" check (which only looks at custom_resources,
565
+ # not costs) wrongly deactivates that method on the next run.
566
+ for method_name in deleted.get("methods", {}):
567
+ st.session_state.costs.get(method_name, {}).pop(deleted["name"], None)
568
  st.session_state.pop("latest_result", None)
569
  st.rerun()
570
  if st.button("Edit", key=f"edit_batch_{global_idx}",
 
768
  if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
769
  for r in st.session_state.resource_caps:
770
  st.session_state.resource_caps[r] = 0.0
771
+ # Purge costs[method][batch_name] entries left by custom resources before
772
+ # clearing them below — otherwise tab4's missing-resource check wrongly
773
+ # deactivates any method that relied on a batch no longer in existence.
774
+ for batch in st.session_state.custom_resources:
775
+ for method_name in batch.get("methods", {}):
776
+ st.session_state.costs.get(method_name, {}).pop(batch["name"], None)
777
  st.session_state.custom_resources = []
778
  st.session_state.enabled_methods = {}
779
  st.success("All resource values reset to 0.")
780
  st.rerun()
781
 
782
  st.markdown('<p class="crra-sub-sub-heading">Import resource values</p>', unsafe_allow_html=True)
783
+ st.markdown(
784
+ "<p class='caption'>Load standard resource availabilities from an Excel file, "
785
+ "or download the current values.</p>",
786
+ unsafe_allow_html=True,
787
+ )
788
+
789
+ with st.container(key="container-btn-resources"):
790
  with open("data/resource_template.xlsx", "rb") as f:
791
  st.download_button(
792
  label="Download Excel Template",
 
796
  icon=":material/download:",
797
  key="download_button_resource_template"
798
  )
799
+ if st.button("Load from Excel", type="tertiary", icon=":material/arrow_upload_progress:", key="btn-load-resources"):
800
+ dialog_load_standard_resources()
801
+ if st.session_state.get("resource_last_loaded_file"):
802
+ st.success(f"Loaded: **{st.session_state['resource_last_loaded_file']}**", icon="✅")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
 
804
  # Standard resource inputs
805
  st.markdown('<p class="crra-sub-heading">Set available resources for one year</p>', unsafe_allow_html=True)
 
863
 
864
  apply_auto_disable_logic(method_to_primary_resources, secondary_resources)
865
 
866
+ # Rendered after all resource inputs above (which write into
867
+ # resource_caps) so the export always reflects this render's latest
868
+ # values — placing it earlier would download the previous rerun's values,
869
+ # since Streamlit executes top-to-bottom and the inputs update the dict
870
+ # further down in the script.
871
+ if any(v > 0 for v in st.session_state.resource_caps.values()):
872
+ st.download_button(
873
+ label="Download current resources",
874
+ data=resource_caps_to_excel_bytes(st.session_state.resource_caps),
875
+ file_name="current_resources.xlsx",
876
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
877
+ icon=":material/download:",
878
+ key="download_button_current_resources",
879
+ )
880
+
881
  panel_custom_resources_controls(cost_sliders)
882
 
883
  render_resource_summary()