""" Budget Advisor — a personal financial-planning tool. Visual direction: editorial private-wealth report. Warm off-white paper, serif headlines, monospace figures, no emoji. All styling is delivered through a custom Gradio CSS override plus Google Fonts (Source Serif 4, Inter, JetBrains Mono). The Gradio theme is kept intentionally neutral so the custom CSS fully controls the look. """ import gradio as gr import json import tempfile from datetime import datetime # ── constants ────────────────────────────────────────────────────────────── EXPENSE_CATS = [ "Housing", "Food & groceries", "Transport", "Utilities", "Entertainment", "Health", "Education", "Clothing", "Other", ] CAT_LIMITS = { "Housing": 30, "Food & groceries": 15, "Transport": 15, "Utilities": 10, "Entertainment": 10, "Health": 10, "Education": 10, "Clothing": 5, "Other": 10, } # Neutral palette of swatches for the stacked-bar + swatch column. CAT_SWATCH = { "Housing": "#1c1a17", "Food & groceries": "#3d3932", "Transport": "#5e5849", "Utilities": "#7a7462", "Entertainment": "#958f7e", "Health": "#a8a28f", "Education": "#b6b09d", "Clothing": "#c4bead", "Other": "#d3cdbb", } FOLLOW_UP_PROMPT = """You are a practical, direct personal finance advisor. I am going to give you a JSON object that summarizes my income, pre-tax deductions, retirement contribution, and monthly expenses. Please do the following: 1. Open with a two- to three-sentence read on the overall health of this budget — what is working, what is concerning. 2. Provide five to six specific, numbered recommendations. Reference the actual dollar amounts and percentages from the JSON. Each should be one or two sentences. 3. Flag any category where spending exceeds the recommended ceiling (the `limit_pct` field), and suggest a realistic target dollar amount. 4. Close with a single highest-priority action for the coming month. Be direct and honest, but constructive. Translate the numbers into decisions. Here is the budget: """ # ── formatting helpers ───────────────────────────────────────────────────── def fmt(n: float) -> str: sign = "−" if n < 0 else "" return f"{sign}${abs(round(n)):,}" def pct(part: float, whole: float, decimals: int = 1) -> str: if whole == 0: return "0.0%" return f"{round(part / whole * 100, decimals)}%" # ── HTML builders (return strings used by gr.HTML blocks) ────────────────── def _kpi_row(gross, takehome, remaining): rem_cls = "accent" if remaining >= 0 else "warn" return f"""
Gross
{fmt(gross)}
100.0%
Take-home
{fmt(takehome)}
{pct(takehome, gross)} of gross
Remaining
{fmt(remaining)}
{pct(remaining, takehome)} of take-home
""" def _flow_table(gross, fed, state, fica, mcare, k401, takehome): return f"""

Income flow

Withholdings & savings
Line itemAmount% gross
Gross income{fmt(gross)}100.0%
Federal tax−{fmt(fed)}{pct(fed, gross)}
State tax−{fmt(state)}{pct(state, gross)}
Social Security−{fmt(fica)}{pct(fica, gross)}
Medicare−{fmt(mcare)}{pct(mcare, gross)}
401(k) contribution−{fmt(k401)}{pct(k401, gross)}
Take-home pay{fmt(takehome)}{pct(takehome, gross)}
""" def _spending_block(used, takehome): # stacked bar segs = [] rows = [] for cat, amt in used.items(): color = CAT_SWATCH.get(cat, "#8a8478") share = (amt / takehome * 100) if takehome > 0 else 0 limit = CAT_LIMITS.get(cat, 10) if share > limit: pill = f'Over {limit}%' elif share > limit * 0.8: pill = f'Near {limit}%' else: pill = "" # bar segments scale against take-home so empty space is remaining segs.append( f'
' ) rows.append(f"""
{cat}
{fmt(amt)}
{share:.1f}%{pill}
""") return f"""

Spending allocation

vs. take-home
{''.join(segs)}
{''.join(rows)}
""" def _position_block(total_exp, remaining, takehome, k401, gross): savings_rate = ((k401 + max(remaining, 0)) / gross * 100) if gross > 0 else 0 return f"""

Position

end of month
Total expenses{fmt(total_exp)}{pct(total_exp, takehome)}
Discretionary surplus{fmt(max(remaining, 0))}{pct(max(remaining, 0), takehome)}
Effective savings rate{fmt(k401 + max(remaining, 0))}{savings_rate:.1f}%
""" def _empty_report(message="Enter your income to generate the report."): return f"""
Advisory report

Awaiting inputs

{message}

""" def _report_header(): now = datetime.now().strftime("%b %Y") return f"""
Advisory report {now}

Monthly Position

Prepared from your declared inputs. Figures rounded to the nearest dollar.

""" # ── core calculation ─────────────────────────────────────────────────────── def calculate_budget( gross_income, fed_tax_pct, state_tax_pct, fica_pct, medicare_pct, k401_pct, housing, food, transport, utilities, entertainment, health, education, clothing, other, ): if gross_income is None or gross_income <= 0: empty_json = "{}" return ( _empty_report("Please enter a valid monthly gross income to generate the report."), empty_json, FOLLOW_UP_PROMPT + empty_json, None, ) fed_amt = gross_income * fed_tax_pct / 100 state_amt = gross_income * state_tax_pct / 100 fica_amt = gross_income * fica_pct / 100 medicare_amt = gross_income * medicare_pct / 100 k401_amt = gross_income * k401_pct / 100 total_tax = fed_amt + state_amt + fica_amt + medicare_amt total_ded = total_tax + k401_amt takehome = gross_income - total_ded expenses = { "Housing": housing, "Food & groceries": food, "Transport": transport, "Utilities": utilities, "Entertainment": entertainment, "Health": health, "Education": education, "Clothing": clothing, "Other": other, } used = {k: (v or 0) for k, v in expenses.items() if v and v > 0} total_exp = sum(used.values()) remaining = takehome - total_exp # ── report html ── report_html = ( _report_header() + _kpi_row(gross_income, takehome, remaining) + _flow_table(gross_income, fed_amt, state_amt, fica_amt, medicare_amt, k401_amt, takehome) + _spending_block(used, takehome) + _position_block(total_exp, remaining, takehome, k401_amt, gross_income) ) # ── structured JSON export ── def _round(x): return round(float(x), 2) expense_breakdown = [] for cat, amt in used.items(): limit = CAT_LIMITS.get(cat, 10) cat_pct = round(amt / takehome * 100, 2) if takehome > 0 else 0 if cat_pct <= limit * 0.8: status = "ok" elif cat_pct <= limit: status = "near_limit" else: status = "over_limit" expense_breakdown.append({ "category": cat, "amount_usd": _round(amt), "pct_of_takehome": cat_pct, "limit_pct": limit, "status": status, }) export_data = { "schema": "budget-ai-advisor/v1", "generated_at": datetime.utcnow().isoformat() + "Z", "currency": "USD", "period": "monthly", "income": {"gross_usd": _round(gross_income)}, "pre_tax_deductions": { "federal_tax_pct": _round(fed_tax_pct), "state_tax_pct": _round(state_tax_pct), "social_security_pct": _round(fica_pct), "medicare_pct": _round(medicare_pct), "retirement_401k_pct": _round(k401_pct), "federal_tax_usd": _round(fed_amt), "state_tax_usd": _round(state_amt), "social_security_usd": _round(fica_amt), "medicare_usd": _round(medicare_amt), "retirement_401k_usd": _round(k401_amt), "total_taxes_usd": _round(total_tax), }, "take_home_usd": _round(takehome), "expenses": expense_breakdown, "totals": { "total_expenses_usd": _round(total_exp), "remaining_usd": _round(remaining), "remaining_pct_of_takehome": round(remaining / takehome * 100, 2) if takehome > 0 else 0, "savings_rate_pct_of_gross": round((k401_amt + max(remaining, 0)) / gross_income * 100, 2) if gross_income > 0 else 0, }, "notes": { "limits_reference": "Each expense's `limit_pct` is a recommended ceiling as a share of " "take-home pay. `status` is 'ok' if under 80% of the limit, " "'near_limit' if 80-100%, 'over_limit' if above.", "savings_rate_definition": "savings_rate_pct_of_gross = (401(k) contribution + leftover after expenses) / gross income", }, } export_json = json.dumps(export_data, indent=2) full_prompt = FOLLOW_UP_PROMPT + export_json timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") tmp_dir = tempfile.gettempdir() download_path = f"{tmp_dir}/budget-export-{timestamp}.json" with open(download_path, "w") as f: f.write(export_json) return report_html, export_json, full_prompt, download_path # ── Custom CSS ───────────────────────────────────────────────────────────── APP_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,400&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap'); :root, .gradio-container { --ba-bg: #faf7f2; --ba-bg-sunk: #f1ecdf; --ba-card: #fffdf8; --ba-ink: #1c1a17; --ba-ink-2: #4a463f; --ba-ink-3: #8a8478; --ba-rule: #e5dfd2; --ba-accent: oklch(0.42 0.05 155); --ba-warn: oklch(0.58 0.12 55); --ba-danger: oklch(0.50 0.15 30); --ba-serif: "Source Serif 4", "Iowan Old Style", Georgia, serif; --ba-sans: "Inter", -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; --ba-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; } /* ── Page shell ───────────────────────────────────────────────── */ html, body, .gradio-container { background: var(--ba-bg) !important; color: var(--ba-ink) !important; font-family: var(--ba-sans) !important; } .gradio-container { max-width: 1240px !important; margin: 0 auto !important; padding: 40px 48px 80px !important; } /* ── Masthead + hero (rendered via gr.HTML) ──────────────────── */ .ba-masthead { border-bottom: 1px solid var(--ba-ink); padding-bottom: 18px; margin-bottom: 28px; display: flex; justify-content: space-between; align-items: baseline; } .ba-brand { font-family: var(--ba-serif); font-weight: 400; font-style: italic; font-size: 22px; letter-spacing: -0.01em; } .ba-brand::before { content: "§"; margin-right: 8px; color: var(--ba-accent); font-style: normal; } .ba-brand-title { font-family: var(--ba-serif); font-style: normal; font-weight: 400; font-size: 15px; color: var(--ba-ink-2); margin-left: 12px; letter-spacing: 0.02em; } .ba-meta { font-family: var(--ba-mono); font-size: 11px; color: var(--ba-ink-3); letter-spacing: 0.08em; text-transform: uppercase; } .ba-hero { margin-bottom: 36px; max-width: 760px; } .ba-hero h1 { font-family: var(--ba-serif); font-weight: 400; font-size: 42px; line-height: 1.1; letter-spacing: -0.015em; margin: 0 0 14px; } .ba-hero h1 em { font-style: italic; color: var(--ba-accent); } .ba-hero p { font-family: var(--ba-serif); font-size: 16px; line-height: 1.55; color: var(--ba-ink-2); margin: 0; max-width: 640px; } /* ── Gradio row / column as a two-column grid feels natural ───── */ .gradio-container .prose { font-family: var(--ba-sans) !important; color: var(--ba-ink) !important; } /* Section headers we render as markdown */ .ba-panel-head { display: flex; justify-content: space-between; align-items: baseline; border-bottom: 1px solid var(--ba-ink); padding-bottom: 8px; margin: 24px 0 14px; } .ba-panel-head h2 { font-family: var(--ba-serif) !important; font-weight: 500 !important; font-size: 20px !important; margin: 0 !important; letter-spacing: -0.005em; color: var(--ba-ink) !important; } .ba-panel-head .ba-idx { font-family: var(--ba-mono); font-size: 10px; letter-spacing: 0.14em; color: var(--ba-ink-3); text-transform: uppercase; } .ba-panel-note { font-family: var(--ba-serif); font-style: italic; font-size: 14px; color: var(--ba-ink-3); margin: -4px 0 14px; } /* ── Gradio input controls ──────────────────────────────────── */ /* Labels */ .gradio-container label span, .gradio-container .label-wrap > span, .gradio-container .block > .label > span, .gradio-container span[data-testid="block-info"] { font-family: var(--ba-sans) !important; font-size: 13px !important; font-weight: 400 !important; color: var(--ba-ink-2) !important; letter-spacing: 0 !important; } /* Number inputs */ .gradio-container input[type="number"], .gradio-container input[type="text"] { font-family: var(--ba-mono) !important; font-size: 14px !important; text-align: right !important; background: var(--ba-card) !important; border: 1px solid var(--ba-rule) !important; color: var(--ba-ink) !important; border-radius: 2px !important; padding: 9px 12px !important; box-shadow: none !important; transition: border-color .15s; } .gradio-container input[type="number"]:focus, .gradio-container input[type="text"]:focus { border-color: var(--ba-ink) !important; outline: none !important; } /* Sliders */ .gradio-container input[type="range"] { accent-color: var(--ba-ink) !important; height: 2px !important; } .gradio-container .wrap.svelte-1cl284s, .gradio-container .wrap .head, .gradio-container .slider_input_container .tick-marks, .gradio-container [data-testid="slider"] { background: transparent !important; } /* Slider value readout — force mono */ .gradio-container .slider_input_container input, .gradio-container [data-testid="slider"] input { font-family: var(--ba-mono) !important; font-size: 12px !important; background: transparent !important; border: none !important; color: var(--ba-ink) !important; text-align: right !important; padding: 0 !important; width: 56px !important; } /* Remove Gradio's default card backgrounds on form blocks */ .gradio-container .block, .gradio-container .form, .gradio-container .gap, .gradio-container fieldset { background: transparent !important; border: none !important; box-shadow: none !important; } /* Give number/slider blocks a bottom rule to feel like a ledger */ .gradio-container .block.svelte-11xb1hd, .gradio-container [data-testid="number-input"], .gradio-container [data-testid="slider"] { border-bottom: 1px solid var(--ba-rule) !important; padding: 12px 0 !important; border-radius: 0 !important; } /* Button: turn Gradio primary into a serious black pill */ .gradio-container button.primary, .gradio-container button[variant="primary"], .gradio-container .primary button { background: var(--ba-ink) !important; color: var(--ba-bg) !important; border: 1px solid var(--ba-ink) !important; border-radius: 2px !important; font-family: var(--ba-sans) !important; font-weight: 500 !important; font-size: 13px !important; letter-spacing: 0.01em !important; padding: 11px 22px !important; box-shadow: none !important; transition: background .15s, border-color .15s; } .gradio-container button.primary:hover, .gradio-container button[variant="primary"]:hover { background: var(--ba-accent) !important; border-color: var(--ba-accent) !important; } .gradio-container button.secondary, .gradio-container button[variant="secondary"] { background: transparent !important; color: var(--ba-ink) !important; border: 1px solid var(--ba-ink) !important; border-radius: 2px !important; font-family: var(--ba-sans) !important; font-weight: 500 !important; font-size: 13px !important; padding: 11px 22px !important; box-shadow: none !important; } .gradio-container button.secondary:hover { background: var(--ba-bg-sunk) !important; } /* ── Results sheet (gr.HTML output) ─────────────────────────── */ .ba-results-sheet { background: var(--ba-card); border: 1px solid var(--ba-rule); padding: 32px 36px; } .ba-report-head { display: flex; justify-content: space-between; align-items: baseline; } .ba-kicker { font-family: var(--ba-mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ba-ink-3); } .ba-report-title { font-family: var(--ba-serif); font-size: 26px; font-weight: 400; letter-spacing: -0.01em; margin: 2px 0 4px; color: var(--ba-ink); } .ba-report-sub { font-family: var(--ba-serif); font-size: 13px; color: var(--ba-ink-3); margin: 0 0 22px; padding-bottom: 18px; border-bottom: 1px solid var(--ba-rule); } .ba-report-empty { padding: 12px 0 8px; } .ba-report-empty .ba-report-title { margin-top: 4px; } .ba-kpi-row { display: grid; grid-template-columns: repeat(3, 1fr); border: 1px solid var(--ba-rule); margin-bottom: 26px; } .ba-kpi { padding: 16px 16px; border-right: 1px solid var(--ba-rule); } .ba-kpi:last-child { border-right: none; } .ba-kpi-label { font-family: var(--ba-mono); font-size: 10px; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ba-ink-3); margin-bottom: 6px; } .ba-kpi-amount { font-family: var(--ba-serif); font-size: 24px; font-weight: 400; letter-spacing: -0.01em; line-height: 1; color: var(--ba-ink); } .ba-kpi-sub { font-family: var(--ba-mono); font-size: 11px; color: var(--ba-ink-3); margin-top: 6px; } .ba-kpi.accent .ba-kpi-amount { color: var(--ba-accent); } .ba-kpi.warn .ba-kpi-amount { color: var(--ba-danger); } .ba-section { margin-bottom: 26px; } .ba-section-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; } .ba-section-head h3 { font-family: var(--ba-serif); font-weight: 500; font-size: 15px; margin: 0; color: var(--ba-ink); } .ba-hint { font-family: var(--ba-mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ba-ink-3); } .ba-ledger { width: 100%; border-collapse: collapse; font-size: 13px; } .ba-ledger th { font-family: var(--ba-mono); font-weight: 500; font-size: 10px; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ba-ink-3); text-align: right; padding: 6px 0; border-bottom: 1px solid var(--ba-rule); } .ba-ledger th:first-child { text-align: left; } .ba-ledger td { padding: 9px 0; border-bottom: 1px dotted var(--ba-rule); font-family: var(--ba-mono); font-size: 12.5px; text-align: right; color: var(--ba-ink); } .ba-ledger td:first-child { text-align: left; font-family: var(--ba-sans); font-size: 13px; color: var(--ba-ink-2); } .ba-ledger tr.ba-total td { border-top: 1px solid var(--ba-ink); border-bottom: none; padding-top: 12px; font-weight: 600; color: var(--ba-ink); } .ba-ledger tr.ba-total td:first-child { font-family: var(--ba-serif); font-weight: 500; font-size: 14px; } .ba-bar { display: flex; height: 10px; width: 100%; background: var(--ba-bg-sunk); margin-bottom: 14px; overflow: hidden; } .ba-seg { height: 100%; border-right: 1px solid var(--ba-card); } .ba-seg:last-child { border-right: none; } .ba-cat-row { display: grid; grid-template-columns: 14px 1fr auto 130px; gap: 12px; align-items: center; padding: 8px 0; border-bottom: 1px dotted var(--ba-rule); font-size: 13px; } .ba-cat-row:last-child { border-bottom: none; } .ba-swatch { width: 10px; height: 10px; } .ba-cat-name { color: var(--ba-ink-2); } .ba-cat-amt { font-family: var(--ba-mono); font-size: 12.5px; color: var(--ba-ink); } .ba-cat-pct { font-family: var(--ba-mono); font-size: 11px; color: var(--ba-ink-3); text-align: right; } .ba-pill { display: inline-block; font-family: var(--ba-mono); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; padding: 2px 6px; border: 1px solid; margin-left: 6px; vertical-align: 2px; } .ba-pill.over { color: var(--ba-danger); border-color: var(--ba-danger); } .ba-pill.near { color: var(--ba-warn); border-color: var(--ba-warn); } /* ── Handoff section ───────────────────────────────────────── */ .ba-handoff-head { border-top: 1px solid var(--ba-ink); padding-top: 28px; margin-top: 40px; } .ba-handoff-head h2 { font-family: var(--ba-serif); font-weight: 400; font-size: 26px; letter-spacing: -0.01em; margin: 0 0 10px; } .ba-handoff-head p { font-family: var(--ba-serif); font-size: 15px; color: var(--ba-ink-2); max-width: 680px; margin: 0 0 22px; } .ba-steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; margin-bottom: 20px; } .ba-step { border-top: 1px solid var(--ba-rule); padding-top: 12px; } .ba-step .ba-n { font-family: var(--ba-mono); font-size: 10px; letter-spacing: 0.14em; color: var(--ba-ink-3); text-transform: uppercase; margin-bottom: 6px; } .ba-step h4 { font-family: var(--ba-serif); font-weight: 500; font-size: 15px; margin: 0 0 4px; color: var(--ba-ink); } .ba-step p { font-family: var(--ba-sans); font-size: 13px; color: var(--ba-ink-2); margin: 0; } /* Gradio Code block override */ .gradio-container .cm-editor, .gradio-container .cm-content, .gradio-container pre, .gradio-container code { font-family: var(--ba-mono) !important; font-size: 12px !important; line-height: 1.55 !important; } .gradio-container .cm-editor { background: #1c1a17 !important; color: #e8e3d9 !important; border: 1px solid var(--ba-rule) !important; border-radius: 2px !important; } /* Accordion styling */ .gradio-container .label-wrap, .gradio-container details > summary { font-family: var(--ba-serif) !important; font-size: 15px !important; font-weight: 500 !important; color: var(--ba-ink) !important; padding: 10px 0 !important; border-bottom: 1px solid var(--ba-rule) !important; } /* File download component */ .gradio-container [data-testid="file"] { background: var(--ba-card) !important; border: 1px solid var(--ba-rule) !important; border-radius: 2px !important; } /* Footer text */ .ba-footer { margin-top: 56px; padding-top: 20px; border-top: 1px solid var(--ba-rule); display: flex; justify-content: space-between; font-family: var(--ba-mono); font-size: 10px; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ba-ink-3); } """ APP_THEME = gr.themes.Base( primary_hue=gr.themes.colors.stone, secondary_hue=gr.themes.colors.stone, neutral_hue=gr.themes.colors.stone, font=("Inter", "system-ui", "sans-serif"), font_mono=("JetBrains Mono", "ui-monospace", "monospace"), ).set( body_background_fill="#faf7f2", body_text_color="#1c1a17", background_fill_primary="#faf7f2", background_fill_secondary="#faf7f2", border_color_primary="#e5dfd2", block_background_fill="transparent", block_border_width="0px", block_label_background_fill="transparent", button_primary_background_fill="#1c1a17", button_primary_text_color="#faf7f2", button_secondary_background_fill="transparent", button_secondary_text_color="#1c1a17", button_secondary_border_color="#1c1a17", input_background_fill="#fffdf8", input_border_color="#e5dfd2", ) # ── Gradio UI ────────────────────────────────────────────────────────────── with gr.Blocks(title="Budget Advisor", theme=APP_THEME, css=APP_CSS) as demo: # Masthead gr.HTML("""
Ledger & Co. Personal Budget Advisor
Monthly · USD

A measured view of your monthly finances.

Enter your income, pre-tax deductions, and expenses below. The report compiles a structured view of where your money goes — and packages it as a handoff you can take to any AI advisor for personalized guidance.

""") with gr.Row(equal_height=False): # ─── LEFT: inputs ───────────────────────────────────────────── with gr.Column(scale=5, min_width=420): gr.HTML('

Income

I.
') gross_income = gr.Number( label="Monthly gross income (USD)", value=5000, minimum=0, precision=0, ) gr.HTML('

Pre-tax deductions

II.
' '

Adjust to reflect your actual withholdings and retirement contributions.

') with gr.Row(): fed_tax = gr.Slider(0, 50, value=22, step=0.5, label="Federal income tax (%)") state_tax = gr.Slider(0, 15, value=5, step=0.5, label="State income tax (%)") with gr.Row(): fica = gr.Slider(0, 10, value=6.2, step=0.1, label="Social Security / FICA (%)") medicare = gr.Slider(0, 5, value=1.45, step=0.05, label="Medicare (%)") k401 = gr.Slider(0, 30, value=6, step=0.5, label="401(k) contribution (%)") gr.HTML('

Monthly expenses

III.
' '

Recurring categories with rough recommended ceilings as a share of take-home.

') with gr.Row(): housing = gr.Number(label="Housing (USD)", value=1200, minimum=0, precision=0) food = gr.Number(label="Food & groceries (USD)", value=400, minimum=0, precision=0) transport = gr.Number(label="Transport (USD)", value=200, minimum=0, precision=0) with gr.Row(): utilities = gr.Number(label="Utilities (USD)", value=150, minimum=0, precision=0) entertainment = gr.Number(label="Entertainment (USD)", value=100, minimum=0, precision=0) health = gr.Number(label="Health (USD)", value=0, minimum=0, precision=0) with gr.Row(): education = gr.Number(label="Education (USD)", value=0, minimum=0, precision=0) clothing = gr.Number(label="Clothing (USD)", value=0, minimum=0, precision=0) other = gr.Number(label="Other (USD)", value=0, minimum=0, precision=0) calc_btn = gr.Button("Compile report", variant="primary", size="lg") # ─── RIGHT: results sheet ───────────────────────────────────── with gr.Column(scale=6, min_width=480): report_html = gr.HTML( value='
' + _empty_report() + '
', ) # Handoff section gr.HTML("""

Take this report further

Your budget has been packaged as a structured handoff — a carefully written prompt followed by your figures as JSON. Paste the whole block into the AI assistant of your choice for a personalized read.

Step 01

Verify your inputs

Adjust the sliders and expense fields until the report reflects your actual situation.

Step 02

Copy the handoff

Use the button below to copy the prompt and JSON in one click, or download the JSON as a file.

Step 03

Paste into your advisor

Any capable assistant — Claude, ChatGPT, or your own — will produce an actionable read.

""") with gr.Accordion("Ready-to-paste prompt and budget data", open=True): out_full_prompt = gr.Code( label="Prompt + JSON (copy the whole block)", language="markdown", interactive=False, lines=18, ) with gr.Row(): copy_btn = gr.Button("Copy handoff to clipboard", variant="primary") out_download = gr.File(label="Or download the JSON", interactive=False) with gr.Accordion("Raw JSON only", open=False): out_json = gr.Code( label="Budget data as JSON", language="json", interactive=False, lines=18, ) gr.HTML('') # ── wire up (computes on button click + on any input change) ─── inputs = [ gross_income, fed_tax, state_tax, fica, medicare, k401, housing, food, transport, utilities, entertainment, health, education, clothing, other, ] outputs = [report_html, out_json, out_full_prompt, out_download] calc_btn.click(fn=calculate_budget, inputs=inputs, outputs=outputs) # Live-update so the report reflects input changes without needing the button for ctrl in inputs: ctrl.change(fn=calculate_budget, inputs=inputs, outputs=outputs) # compute initial report on load demo.load(fn=calculate_budget, inputs=inputs, outputs=outputs) # Clipboard copy runs in the browser — no server round-trip needed. copy_btn.click( fn=None, inputs=[out_full_prompt], outputs=[], js=""" (text) => { if (!text) { alert("Adjust your inputs first, then try again."); return; } navigator.clipboard.writeText(text).then(() => { alert("Copied. Paste it into your AI assistant of choice."); }).catch(() => { alert("Couldn't copy automatically — select the text in the box and copy it manually."); }); } """, ) if __name__ == "__main__": demo.launch()