""" app.py — Smart Budget Planner (Gradio) Hugging Face Space entry point. Run locally: pip install gradio python app.py """ import gradio as gr # ───────────────────────────────────────── # Python calculation engine # ───────────────────────────────────────── def calculate_budget( gross, fed_tax, fica, retire, health, rent, electric, internet, phone, groceries, dining, transport, subs, personal, emergency, invest, vacation, ): """ Pure-Python budget engine. Returns a dict of all derived metrics. """ gross = float(gross or 0) # ── Deductions (pre-tax) ── ded_total = float(fed_tax + fica + retire + health) net = gross - ded_total # ── Housing ── housing_total = float(rent + electric + internet + phone) # ── Living ── living_total = float(groceries + dining + transport + subs + personal) # ── Goals ── goals_total = float(emergency + invest + vacation) # ── Fixed vs variable ── fixed_total = float( fed_tax + fica + retire + health + # deductions always fixed rent + internet + phone + # fixed housing subs + # fixed living emergency + invest + vacation # goals always fixed ) var_total = float(electric + groceries + dining + transport + personal) total_exp = housing_total + living_total + goals_total leftover = net - housing_total - living_total - goals_total # ── Budget score 0–100 ── if net > 0: score = max(0, min(100, round( (leftover / net) * 60 + (goals_total / net) * 40 * 3 ))) else: score = 0 def pct(val, base): return round(val / base * 100) if base > 0 else 0 def fmt(val): return f"${abs(round(val)):,}" # ── Score label ── if score >= 70: score_label = "Healthy" score_color = "green" elif score >= 40: score_label = "Fair — room to improve" score_color = "orange" else: score_label = "Needs attention" score_color = "red" leftover_label = "Over budget!" if leftover < 0 else f"{pct(leftover, net)}% unallocated" # ── Suggestions ── suggestions = [] if pct(dining, net) > 8: suggestions.append(("⚠️", f"Dining out is {pct(dining, net)}% of net. Trimming to 6% would save {fmt(dining - net * 0.06)}/mo.")) else: suggestions.append(("✅", f"Dining spend is healthy at {pct(dining, net)}% of net income.")) if pct(retire, gross) < 10: suggestions.append(("⚠️", f"Retirement is {pct(retire, gross)}% of gross. Target 10–15% for long-term security.")) else: suggestions.append(("✅", f"Retirement contribution is {pct(retire, gross)}% of gross — solid.")) if pct(emergency, net) < 5: suggestions.append(("🔴", f"Emergency fund is only {pct(emergency, net)}% of net. Aim for at least 5% monthly.")) else: suggestions.append(("✅", f"Emergency savings rate is on track at {pct(emergency, net)}% of net.")) if leftover < 0: suggestions.append(("🔴", f"Over budget by {fmt(abs(leftover))}. Review variable expenses to find savings.")) elif pct(leftover, net) > 15: suggestions.append(("✅", f"{pct(leftover, net)}% unallocated ({fmt(leftover)}). Consider boosting investments or savings goals.")) if pct(fixed_total, net) > 72: suggestions.append(("⚠️", f"Fixed costs are {pct(fixed_total, net)}% of net — high. This reduces financial flexibility.")) suggestions_md = "\n".join(f"{icon} {text}" for icon, text in suggestions[:5]) return { # metric cards "net": fmt(net), "net_pct": f"{pct(net, gross)}% of gross", "total_exp": fmt(total_exp), "total_exp_sub":f"{pct(total_exp, net)}% of net · Fixed {pct(fixed_total, net)}% · Variable {pct(var_total, net)}%", "score": f"{score} / 100", "score_label": score_label, "leftover": ('' if leftover >= 0 else '-') + fmt(leftover), "leftover_sub": leftover_label, # chart data (raw numbers for Plotly) "chart_deductions": round(ded_total), "chart_housing": round(housing_total), "chart_living": round(living_total), "chart_goals": round(goals_total), "chart_leftover": round(max(0, leftover)), # expense % breakdown for the table "pct_rent": pct(rent, net), "pct_groceries": pct(groceries, net), "pct_dining": pct(dining, net), "pct_retire": pct(retire, gross), "pct_emergency": pct(emergency, net), # suggestions "suggestions": suggestions_md, # raw values for What-if echo "net_raw": round(net), "leftover_raw": round(leftover), "score_raw": score, } def run_budget(*args): """ Gradio callback — receives all slider/number inputs, returns formatted output strings for each Output component. """ r = calculate_budget(*args) metric_md = f""" ### 📊 Budget summary | Metric | Value | Detail | |--------|-------|--------| | Take-home (net) | **{r['net']}** | {r['net_pct']} | | Total expenses | **{r['total_exp']}** | {r['total_exp_sub']} | | Budget score | **{r['score']}** | {r['score_label']} | | Leftover | **{r['leftover']}** | {r['leftover_sub']} | """ suggestions_md = f"### 💡 Suggestions\n\n{r['suggestions']}" return metric_md, suggestions_md def run_whatif( gross, fed_tax, fica, retire, health, rent, electric, internet, phone, groceries, dining, transport, subs, personal, emergency, invest, vacation, wi_rent, wi_groceries, wi_dining, wi_transport, wi_dining_out, wi_electric, wi_personal, wi_subs, wi_emergency, wi_invest, wi_vacation, ): """ What-if callback — compares original vs adjusted values. """ orig = calculate_budget( gross, fed_tax, fica, retire, health, rent, electric, internet, phone, groceries, dining, transport, subs, personal, emergency, invest, vacation, ) # Build adjusted inputs by substituting what-if overrides adj = calculate_budget( gross, fed_tax, fica, retire, health, wi_rent, wi_electric, internet, phone, wi_groceries, wi_dining_out, wi_transport, wi_subs, wi_personal, wi_emergency, wi_invest, wi_vacation, ) left_delta = adj["leftover_raw"] - orig["leftover_raw"] score_delta = adj["score_raw"] - orig["score_raw"] sign = lambda v: ("+" if v >= 0 else "") + f"${abs(v):,}" impact_md = f""" ### ⚡ Ripple impact | | Original | Adjusted | Change | |--|---------|----------|--------| | Leftover | **{orig['leftover']}** | **{adj['leftover']}** | {sign(left_delta)} | | Budget score | **{orig['score']}** | **{adj['score']}** | {sign(score_delta).replace('$','')} pts | | Total expenses | **{orig['total_exp']}** | **{adj['total_exp']}** | — | {adj['suggestions']} """ return impact_md # ───────────────────────────────────────── # Gradio UI # ───────────────────────────────────────── THEME = gr.themes.Soft( primary_hue="green", secondary_hue="blue", neutral_hue="slate", font=[gr.themes.GoogleFont("DM Sans"), "ui-sans-serif", "system-ui"], font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"], ) CSS = """ /* ── General ── */ .gradio-container { max-width: 1100px !important; margin: 0 auto; } h1.app-title { font-size: 28px; font-weight: 700; margin-bottom: 2px; } p.app-sub { color: #6b7280; font-size: 14px; margin-bottom: 1.5rem; } /* ── Section headers ── */ .section-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #6b7280; margin-bottom: 6px; } /* ── Badge-style group labels ── */ .badge-fixed { color: #0C447C; background: #E6F1FB; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; } .badge-variable { color: #633806; background: #FAEEDA; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; } .badge-pretax { color: #72243E; background: #FBEAF0; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; } .badge-goals { color: #3C3489; background: #EEEDFE; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; } /* ── Output markdown ── */ .output-box { background: #f8fafc; border-radius: 12px; padding: 1rem 1.25rem; border: 1px solid #e2e8f0; } """ with gr.Blocks(theme=THEME, css=CSS, title="Smart Budget Planner") as demo: # ── Header ── gr.HTML("""
Track · Analyze · Plan — edit any field and click Calculate