Spaces:
Running
Running
Commit ·
1c54d69
1
Parent(s): 138265a
Redesign sidebar and add LiiS logo
Browse files- Add view intro headers (audience + insights) to all 5 views
- Move view selection above product, change to dropdown
- Remove sidebar dividers
- Add LiiS logo top-right of main view
- Remove Repair/Scrap Rate cards, simplify to Grade A + Repair Cost row
- Repairability score 0-10 scale
- Fix Return Grade chart y-axis to 0-100
- Bump view description text size
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- app.py +42 -56
- liis branding/pasted-image-1.png +3 -0
app.py
CHANGED
|
@@ -72,7 +72,11 @@ st.markdown(f"""
|
|
| 72 |
""", unsafe_allow_html=True)
|
| 73 |
|
| 74 |
# Header
|
| 75 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
# Load product configs
|
|
@@ -92,6 +96,15 @@ products = load_products()
|
|
| 92 |
# ============================================================
|
| 93 |
# SIDEBAR - Product Selection & View Selection
|
| 94 |
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
st.sidebar.header("Product Selection")
|
| 96 |
selected_product = st.sidebar.selectbox(
|
| 97 |
"Choose a product",
|
|
@@ -103,16 +116,6 @@ config = products[selected_product]
|
|
| 103 |
if "image_url" in products[selected_product].get("product", {}):
|
| 104 |
st.sidebar.image(products[selected_product]["product"]["image_url"], use_column_width=True)
|
| 105 |
|
| 106 |
-
st.sidebar.markdown("---")
|
| 107 |
-
st.sidebar.header("View")
|
| 108 |
-
selected_view = st.sidebar.radio(
|
| 109 |
-
"Select stakeholder view",
|
| 110 |
-
options=["Strategy", "Design", "Logistics", "Sourcing", "Market"],
|
| 111 |
-
index=0,
|
| 112 |
-
help="Each view shows relevant controls and insights"
|
| 113 |
-
)
|
| 114 |
-
|
| 115 |
-
st.sidebar.markdown("---")
|
| 116 |
|
| 117 |
# ============================================================
|
| 118 |
# TAB-SPECIFIC SIDEBAR CONTROLS
|
|
@@ -191,7 +194,7 @@ elif selected_view == "Design":
|
|
| 191 |
if "design_damage_rate" not in st.session_state:
|
| 192 |
st.session_state.design_damage_rate = 10
|
| 193 |
if "design_repairability" not in st.session_state:
|
| 194 |
-
st.session_state.design_repairability =
|
| 195 |
if "design_spare_parts" not in st.session_state:
|
| 196 |
st.session_state.design_spare_parts = 75
|
| 197 |
|
|
@@ -201,9 +204,9 @@ elif selected_view == "Design":
|
|
| 201 |
help="% of products needing repair per cycle"
|
| 202 |
)
|
| 203 |
st.sidebar.slider(
|
| 204 |
-
"Repairability Score",
|
| 205 |
key="design_repairability",
|
| 206 |
-
help="
|
| 207 |
)
|
| 208 |
st.sidebar.slider(
|
| 209 |
"Spare Parts Availability", 0, 100,
|
|
@@ -213,7 +216,7 @@ elif selected_view == "Design":
|
|
| 213 |
|
| 214 |
# Sync back to shared state so other views stay consistent
|
| 215 |
_damage = st.session_state.design_damage_rate / 100
|
| 216 |
-
_repair_factor = st.session_state.design_repairability /
|
| 217 |
_spare_factor = st.session_state.design_spare_parts / 100
|
| 218 |
st.session_state.refurb_needed_pct = _damage * _repair_factor * _spare_factor + (_damage - _damage * _repair_factor * _spare_factor)
|
| 219 |
|
|
@@ -363,7 +366,11 @@ def calculate_breakeven_months(portfolio, monthly_lease, mfg_cost_nok, lease_ter
|
|
| 363 |
# VIEW: STRATEGY (C-Level)
|
| 364 |
# ============================================================
|
| 365 |
if selected_view == "Strategy":
|
| 366 |
-
st.header("
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
# Get Strategy slider values
|
| 369 |
portfolio_size = st.session_state.strategy_portfolio
|
|
@@ -651,11 +658,13 @@ if selected_view == "Strategy":
|
|
| 651 |
# VIEW: DESIGN
|
| 652 |
# ============================================================
|
| 653 |
elif selected_view == "Design":
|
| 654 |
-
st.header("
|
|
|
|
|
|
|
| 655 |
|
| 656 |
# ── Design-specific calculations from 3 sliders ──
|
| 657 |
d_damage_rate = st.session_state.design_damage_rate / 100
|
| 658 |
-
d_repairability_factor = st.session_state.design_repairability /
|
| 659 |
d_spare_parts_factor = st.session_state.design_spare_parts / 100
|
| 660 |
|
| 661 |
d_grade_a_pct = 1 - d_damage_rate
|
|
@@ -701,46 +710,22 @@ elif selected_view == "Design":
|
|
| 701 |
|
| 702 |
st.markdown('<div style="margin-top: 1.5rem;"></div>', unsafe_allow_html=True)
|
| 703 |
|
| 704 |
-
# ── 4c. Supporting metrics (
|
|
|
|
| 705 |
sup_left, sup_right = st.columns(2)
|
| 706 |
|
| 707 |
with sup_left:
|
| 708 |
-
st.markdown(f'<p style="color: {COLORS["text_muted"]}; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.5rem;">Repair & Recovery</p>', unsafe_allow_html=True)
|
| 709 |
-
# Repair Rate
|
| 710 |
-
st.markdown(f"""
|
| 711 |
-
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']};
|
| 712 |
-
border-radius: 10px; border: 1px solid {COLORS['border']}; margin-bottom: 0.75rem;">
|
| 713 |
-
<p style="color: {COLORS['text_muted']}; margin: 0 0 0.5rem 0;
|
| 714 |
-
font-size: 1.2rem; font-weight: 700;">Repair Rate</p>
|
| 715 |
-
<p style="color: {COLORS['warning']}; margin: 0;
|
| 716 |
-
font-size: 2.4rem; font-weight: 700;">{d_grade_b_pct:.1%}</p>
|
| 717 |
-
</div>
|
| 718 |
-
""", unsafe_allow_html=True)
|
| 719 |
-
# Scrap Rate
|
| 720 |
st.markdown(f"""
|
| 721 |
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']};
|
| 722 |
border-radius: 10px; border: 1px solid {COLORS['border']};">
|
| 723 |
-
<p style="color: {COLORS['text_muted']}; margin: 0 0 0.5rem 0;
|
| 724 |
-
font-size: 1.2rem; font-weight: 700;">Scrap Rate</p>
|
| 725 |
-
<p style="color: {COLORS['negative']}; margin: 0;
|
| 726 |
-
font-size: 2.4rem; font-weight: 700;">{d_grade_c_pct:.1%}</p>
|
| 727 |
-
</div>
|
| 728 |
-
""", unsafe_allow_html=True)
|
| 729 |
-
|
| 730 |
-
with sup_right:
|
| 731 |
-
st.markdown(f'<p style="color: {COLORS["text_muted"]}; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.5rem;">Waste & Quality</p>', unsafe_allow_html=True)
|
| 732 |
-
# Grade A Rate
|
| 733 |
-
st.markdown(f"""
|
| 734 |
-
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']};
|
| 735 |
-
border-radius: 10px; border: 1px solid {COLORS['border']}; margin-bottom: 0.75rem;">
|
| 736 |
<p style="color: {COLORS['text_muted']}; margin: 0 0 0.5rem 0;
|
| 737 |
font-size: 1.2rem; font-weight: 700;">Grade A Rate</p>
|
| 738 |
<p style="color: {COLORS['positive']}; margin: 0;
|
| 739 |
font-size: 2.4rem; font-weight: 700;">{d_grade_a_pct:.1%}</p>
|
| 740 |
</div>
|
| 741 |
""", unsafe_allow_html=True)
|
| 742 |
-
|
| 743 |
-
|
| 744 |
st.markdown(f"""
|
| 745 |
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']};
|
| 746 |
border-radius: 10px; border: 1px solid {COLORS['border']};">
|
|
@@ -748,8 +733,6 @@ elif selected_view == "Design":
|
|
| 748 |
font-size: 1.2rem; font-weight: 700;">Repair Cost per Unit</p>
|
| 749 |
<p style="color: {COLORS['secondary']}; margin: 0;
|
| 750 |
font-size: 2.4rem; font-weight: 700;">{sfmt(int(repair_cost_nok))} <span style="font-size: 1.1rem; font-weight: 400; color: {COLORS['text_subtle']};">NOK</span></p>
|
| 751 |
-
<p style="color: {COLORS['text_subtle']}; margin: 0.3rem 0 0 0;
|
| 752 |
-
font-size: 0.8rem;">(Affected by repairability score)</p>
|
| 753 |
</div>
|
| 754 |
""", unsafe_allow_html=True)
|
| 755 |
|
|
@@ -771,11 +754,11 @@ elif selected_view == "Design":
|
|
| 771 |
title="Return Grade Distribution", text="Percentage",
|
| 772 |
)
|
| 773 |
fig_grades.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
|
| 774 |
-
fig_grades.update_layout(showlegend=False, yaxis_title="% of Returns")
|
| 775 |
st.plotly_chart(fig_grades)
|
| 776 |
|
| 777 |
st.info(f"**Design insight:** At {st.session_state.design_damage_rate}% damage rate with repairability score "
|
| 778 |
-
f"{st.session_state.design_repairability}/
|
| 779 |
f"**{d_grade_b_pct:.1%}** of products can be repaired while **{d_grade_c_pct:.1%}** are scrapped.")
|
| 780 |
|
| 781 |
st.markdown("---")
|
|
@@ -886,8 +869,9 @@ elif selected_view == "Design":
|
|
| 886 |
# VIEW: LOGISTICS
|
| 887 |
# ============================================================
|
| 888 |
elif selected_view == "Logistics":
|
| 889 |
-
st.header("Logistics
|
| 890 |
-
st.markdown("
|
|
|
|
| 891 |
|
| 892 |
# Return assumptions summary
|
| 893 |
st.subheader("Return Flow Assumptions")
|
|
@@ -1003,8 +987,9 @@ elif selected_view == "Logistics":
|
|
| 1003 |
# VIEW: SOURCING
|
| 1004 |
# ============================================================
|
| 1005 |
elif selected_view == "Sourcing":
|
| 1006 |
-
st.header("Sourcing
|
| 1007 |
-
st.markdown("
|
|
|
|
| 1008 |
|
| 1009 |
has_materials = "materials" in config and "bom" in config.get("materials", {})
|
| 1010 |
has_labor = "labor" in config
|
|
@@ -1133,8 +1118,9 @@ elif selected_view == "Sourcing":
|
|
| 1133 |
# VIEW: MARKET
|
| 1134 |
# ============================================================
|
| 1135 |
elif selected_view == "Market":
|
| 1136 |
-
st.header("Market
|
| 1137 |
-
st.markdown("
|
|
|
|
| 1138 |
|
| 1139 |
# Customer Value Proposition
|
| 1140 |
st.subheader("Customer Value Proposition")
|
|
|
|
| 72 |
""", unsafe_allow_html=True)
|
| 73 |
|
| 74 |
# Header
|
| 75 |
+
header_left, header_right = st.columns([3, 1])
|
| 76 |
+
with header_left:
|
| 77 |
+
st.title("LiiS Circular Economy Simulator")
|
| 78 |
+
with header_right:
|
| 79 |
+
st.image("liis branding/pasted-image-1.png", width=120)
|
| 80 |
|
| 81 |
|
| 82 |
# Load product configs
|
|
|
|
| 96 |
# ============================================================
|
| 97 |
# SIDEBAR - Product Selection & View Selection
|
| 98 |
# ============================================================
|
| 99 |
+
st.sidebar.header("View")
|
| 100 |
+
selected_view = st.sidebar.selectbox(
|
| 101 |
+
"Select stakeholder view",
|
| 102 |
+
options=["Strategy", "Design", "Logistics", "Sourcing", "Market"],
|
| 103 |
+
index=0,
|
| 104 |
+
help="Each view shows relevant controls and insights"
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
st.sidebar.header("Product Selection")
|
| 109 |
selected_product = st.sidebar.selectbox(
|
| 110 |
"Choose a product",
|
|
|
|
| 116 |
if "image_url" in products[selected_product].get("product", {}):
|
| 117 |
st.sidebar.image(products[selected_product]["product"]["image_url"], use_column_width=True)
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
# ============================================================
|
| 121 |
# TAB-SPECIFIC SIDEBAR CONTROLS
|
|
|
|
| 194 |
if "design_damage_rate" not in st.session_state:
|
| 195 |
st.session_state.design_damage_rate = 10
|
| 196 |
if "design_repairability" not in st.session_state:
|
| 197 |
+
st.session_state.design_repairability = 7
|
| 198 |
if "design_spare_parts" not in st.session_state:
|
| 199 |
st.session_state.design_spare_parts = 75
|
| 200 |
|
|
|
|
| 204 |
help="% of products needing repair per cycle"
|
| 205 |
)
|
| 206 |
st.sidebar.slider(
|
| 207 |
+
"Repairability Score", 0, 10,
|
| 208 |
key="design_repairability",
|
| 209 |
+
help="0 = everything scrapped, 10 = everything can be repaired"
|
| 210 |
)
|
| 211 |
st.sidebar.slider(
|
| 212 |
"Spare Parts Availability", 0, 100,
|
|
|
|
| 216 |
|
| 217 |
# Sync back to shared state so other views stay consistent
|
| 218 |
_damage = st.session_state.design_damage_rate / 100
|
| 219 |
+
_repair_factor = st.session_state.design_repairability / 10
|
| 220 |
_spare_factor = st.session_state.design_spare_parts / 100
|
| 221 |
st.session_state.refurb_needed_pct = _damage * _repair_factor * _spare_factor + (_damage - _damage * _repair_factor * _spare_factor)
|
| 222 |
|
|
|
|
| 366 |
# VIEW: STRATEGY (C-Level)
|
| 367 |
# ============================================================
|
| 368 |
if selected_view == "Strategy":
|
| 369 |
+
st.header("Strategy")
|
| 370 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0; font-size: 1.15rem;">For C-level executives and business owners</p>', unsafe_allow_html=True)
|
| 371 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0 0 1.5rem 0; font-size: 1.15rem;">Portfolio economics, break-even timeline, and operating margin</p>', unsafe_allow_html=True)
|
| 372 |
+
|
| 373 |
+
st.subheader("Portfolio Economics")
|
| 374 |
|
| 375 |
# Get Strategy slider values
|
| 376 |
portfolio_size = st.session_state.strategy_portfolio
|
|
|
|
| 658 |
# VIEW: DESIGN
|
| 659 |
# ============================================================
|
| 660 |
elif selected_view == "Design":
|
| 661 |
+
st.header("Design")
|
| 662 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0; font-size: 1.15rem;">For product designers and sustainability officers</p>', unsafe_allow_html=True)
|
| 663 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0 0 1.5rem 0; font-size: 1.15rem;">Recovery rate, repairability impact, and component failure analysis</p>', unsafe_allow_html=True)
|
| 664 |
|
| 665 |
# ── Design-specific calculations from 3 sliders ──
|
| 666 |
d_damage_rate = st.session_state.design_damage_rate / 100
|
| 667 |
+
d_repairability_factor = st.session_state.design_repairability / 10
|
| 668 |
d_spare_parts_factor = st.session_state.design_spare_parts / 100
|
| 669 |
|
| 670 |
d_grade_a_pct = 1 - d_damage_rate
|
|
|
|
| 710 |
|
| 711 |
st.markdown('<div style="margin-top: 1.5rem;"></div>', unsafe_allow_html=True)
|
| 712 |
|
| 713 |
+
# ── 4c. Supporting metrics (row) ──
|
| 714 |
+
repair_cost_nok = d_repair_cost * 10 # EUR to NOK approx
|
| 715 |
sup_left, sup_right = st.columns(2)
|
| 716 |
|
| 717 |
with sup_left:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
st.markdown(f"""
|
| 719 |
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']};
|
| 720 |
border-radius: 10px; border: 1px solid {COLORS['border']};">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
<p style="color: {COLORS['text_muted']}; margin: 0 0 0.5rem 0;
|
| 722 |
font-size: 1.2rem; font-weight: 700;">Grade A Rate</p>
|
| 723 |
<p style="color: {COLORS['positive']}; margin: 0;
|
| 724 |
font-size: 2.4rem; font-weight: 700;">{d_grade_a_pct:.1%}</p>
|
| 725 |
</div>
|
| 726 |
""", unsafe_allow_html=True)
|
| 727 |
+
|
| 728 |
+
with sup_right:
|
| 729 |
st.markdown(f"""
|
| 730 |
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']};
|
| 731 |
border-radius: 10px; border: 1px solid {COLORS['border']};">
|
|
|
|
| 733 |
font-size: 1.2rem; font-weight: 700;">Repair Cost per Unit</p>
|
| 734 |
<p style="color: {COLORS['secondary']}; margin: 0;
|
| 735 |
font-size: 2.4rem; font-weight: 700;">{sfmt(int(repair_cost_nok))} <span style="font-size: 1.1rem; font-weight: 400; color: {COLORS['text_subtle']};">NOK</span></p>
|
|
|
|
|
|
|
| 736 |
</div>
|
| 737 |
""", unsafe_allow_html=True)
|
| 738 |
|
|
|
|
| 754 |
title="Return Grade Distribution", text="Percentage",
|
| 755 |
)
|
| 756 |
fig_grades.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
|
| 757 |
+
fig_grades.update_layout(showlegend=False, yaxis_title="% of Returns", yaxis=dict(range=[0, 100]))
|
| 758 |
st.plotly_chart(fig_grades)
|
| 759 |
|
| 760 |
st.info(f"**Design insight:** At {st.session_state.design_damage_rate}% damage rate with repairability score "
|
| 761 |
+
f"{st.session_state.design_repairability}/10, "
|
| 762 |
f"**{d_grade_b_pct:.1%}** of products can be repaired while **{d_grade_c_pct:.1%}** are scrapped.")
|
| 763 |
|
| 764 |
st.markdown("---")
|
|
|
|
| 869 |
# VIEW: LOGISTICS
|
| 870 |
# ============================================================
|
| 871 |
elif selected_view == "Logistics":
|
| 872 |
+
st.header("Logistics")
|
| 873 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0; font-size: 1.15rem;">For logistics managers and warehouse operations</p>', unsafe_allow_html=True)
|
| 874 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0 0 1.5rem 0; font-size: 1.15rem;">Return flow, grade distribution, cost per cycle, and fleet tracking</p>', unsafe_allow_html=True)
|
| 875 |
|
| 876 |
# Return assumptions summary
|
| 877 |
st.subheader("Return Flow Assumptions")
|
|
|
|
| 987 |
# VIEW: SOURCING
|
| 988 |
# ============================================================
|
| 989 |
elif selected_view == "Sourcing":
|
| 990 |
+
st.header("Sourcing")
|
| 991 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0; font-size: 1.15rem;">For procurement, supply chain, and finance</p>', unsafe_allow_html=True)
|
| 992 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0 0 1.5rem 0; font-size: 1.15rem;">Bill of materials, recycled content impact, and manufacturing cost breakdown</p>', unsafe_allow_html=True)
|
| 993 |
|
| 994 |
has_materials = "materials" in config and "bom" in config.get("materials", {})
|
| 995 |
has_labor = "labor" in config
|
|
|
|
| 1118 |
# VIEW: MARKET
|
| 1119 |
# ============================================================
|
| 1120 |
elif selected_view == "Market":
|
| 1121 |
+
st.header("Market")
|
| 1122 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0; font-size: 1.15rem;">For marketing, sales, and business development</p>', unsafe_allow_html=True)
|
| 1123 |
+
st.markdown(f'<p style="color: {COLORS["text_muted"]}; margin: 0 0 1.5rem 0; font-size: 1.15rem;">Customer value proposition, pricing analysis, and environmental impact story</p>', unsafe_allow_html=True)
|
| 1124 |
|
| 1125 |
# Customer Value Proposition
|
| 1126 |
st.subheader("Customer Value Proposition")
|
liis branding/pasted-image-1.png
ADDED
|
Git LFS Details
|