ZakyF commited on
Commit
def3f23
·
1 Parent(s): 641143e

refactor: Rename core engine class, refine expense classification keywords and logic, and update NBO recommendations and risk score weighting.

Browse files
Files changed (1) hide show
  1. app.py +75 -92
app.py CHANGED
@@ -9,8 +9,7 @@ from google import genai
9
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
10
  client_ai = genai.Client(api_key=GOOGLE_API_KEY) if GOOGLE_API_KEY else None
11
 
12
- # --- UI STYLE BANK NAGARI (BERDASARKAN PALET NAGARI.PNG) ---
13
- # Primary: #05A4DE, Light Blue: #82C3EB, Pale: #E0EDF4, White: #FFFFFF, Accent: #F7BD87
14
  custom_css = """
15
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
16
  body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
@@ -20,7 +19,7 @@ body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !importan
20
  padding: 35px; border-radius: 15px; border-bottom: 6px solid #F7BD87;
21
  margin-bottom: 25px; text-align: center;
22
  }
23
- .nagari-header h1 { color: #FFFFFF !important; font-weight: 800 !important; margin: 0; font-size: 2.2em; text-shadow: 2px 2px 4px rgba(0,0,0,0.15); }
24
 
25
  .card-sidebar {
26
  background: #E0EDF4; border-radius: 15px; padding: 25px;
@@ -28,19 +27,15 @@ body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !importan
28
  }
29
  .health-badge { background: white; padding: 12px; border-radius: 8px; margin-bottom: 12px; border-left: 5px solid #05A4DE; font-size: 0.95em; }
30
 
31
- .report-card {
32
- background: white; border-radius: 12px; padding: 30px;
33
- border: 1px solid #E2E8F0; line-height: 1.8; color: #1e293b;
34
- }
35
  .nbo-box { background: #fffdf0; border: 2px solid #F7BD87; padding: 20px; border-radius: 10px; margin-top: 20px; }
36
  """
37
 
38
- class ArchonNagariEngine:
39
  def __init__(self):
40
  self.load_data()
41
 
42
  def load_data(self):
43
- # FASE 1: DATA FOUNDATION [cite: 1]
44
  try:
45
  self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).sort_values('date')
46
  self.df_cust = pd.read_csv('customers.csv')
@@ -58,74 +53,67 @@ class ArchonNagariEngine:
58
  if u_txn.empty or u_info_df.empty: return None
59
  u_info = u_info_df.iloc[0]
60
 
61
- # --- FASE 2: TRANSACTION INTELLIGENCE (Robust Matching) [cite: 7] ---
62
- # Memperluas deteksi agar tidak 100% Discretionary
63
- essential_list = ['grocer', 'utilit', 'transport', 'health', 'educat', 'bill', 'loan', 'salary', 'listrik', 'pdam', 'sekolah']
64
 
65
  def classify_exp(row):
66
- cat = str(row.get('merchant_category', '')).lower()
67
- purp = str(row.get('purpose_code', '')).lower()
68
  desc = str(row.get('raw_description', '')).lower()
69
- if any(k in cat for k in essential_list) or any(k in purp for k in essential_list) or any(k in desc for k in essential_list):
70
- return 'essential'
71
  return 'discretionary'
72
 
73
  u_txn['expense_type'] = u_txn.apply(classify_exp, axis=1)
74
 
75
- # --- FASE 3 & 4: RISK SCORING (STRICT 30/20/20/20/10) [cite: 92, 122] ---
76
- income_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
77
- ref_income = max(income_txn, u_info['monthly_income'])
78
- expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
79
- er = min(expense / ref_income, 1.0) if ref_income > 0 else 1.0
80
 
81
- # Scoring [cite: 139]
82
- er_score = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
83
- bt_score = 1.0 if len(u_bal) >= 2 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[-2]['avg_balance'] else 0.0
84
- od_score = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
85
- mp_score = 1.0 if (u_rep['status'] == 'late').any() else 0.0
86
 
87
- score = (0.3 * er_score) + (0.2 * bt_score) + (0.2 * od_score) + (0.2 * mp_score) + 0.1
88
  risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")
89
 
90
- # --- FASE 5: NBO ENGINE (Customer Oriented) [cite: 167] ---
91
- if risk_lv == "HIGH" or mp_score == 1:
92
- action, desc = "Program Restrukturisasi", "Demi kenyamanan finansial Anda, kami merekomendasikan penyesuaian jadwal pembayaran cicilan."
93
- steps = ["Ajukan peninjauan tenor pinjaman", "Konsolidasi tagihan harian", "Manfaatkan konsultasi dana darurat."]
94
  elif er > 0.6:
95
- action, desc = "Pengendalian Pengeluaran (Spending Control)", "Kami mendeteksi pola belanja yang meningkat. Anda dapat mengatur batas harian transaksi agar tabungan tetap terjaga."
96
- steps = ["Atur limit harian QRIS", "Aktifkan notifikasi saldo minimum", "Prioritaskan kebutuhan pokok (Essential)."]
97
  elif risk_lv == "LOW":
98
- action, desc = "Optimalkan Tabungan (Promote Saving)", "Kondisi keuangan Anda sangat prima. Ini waktu yang tepat untuk menumbuhkan aset Anda lebih maksimal."
99
- steps = ["Buka tabungan berjangka", "Pindahkan dana mengendap ke deposito", "Eksplorasi produk reksa dana."]
100
  else:
101
- action, desc = "Edukasi Finansial Berkala", "Mari perkuat resiliensi keuangan Anda dengan tips pengelolaan arus kas bulanan."
102
- steps = ["Baca artikel cerdas belanja", "Gunakan fitur budgeting di aplikasi", "Review pengeluaran akhir bulan."]
103
 
104
- return risk_lv, score, er, u_bal, u_txn, expense, ref_income, action, desc, steps, er_score, bt_score, od_score, mp_score
105
 
106
- def build_report(self, risk_lv, score, er, u_bal, exp, inc, action, desc, steps, cid, u_txn):
107
- # FASE 6: EXPLAINABLE SUMMARY [cite: 280]
108
- report = f"### LAPORAN ANALISIS RESILIENSI ANDA ({cid})\n\n"
109
- report += f"Halo, Bapak/Ibu. Berdasarkan riwayat transaksi terakhir, tingkat kesehatan finansial Anda saat ini berada pada kategori **{risk_lv}** (Skor: {score:.2f}).\n\n"
110
 
111
- report += f"**Mengapa Hasil Ini Muncul?**\n"
112
- report += f"* **Rasio Belanja ({er:.1%})**: Dari total daya beli Bapak/Ibu (Rp{inc:,.0f}), sebanyak Rp{exp:,.0f} dialokasikan untuk pengeluaran. "
113
- report += "Ini mengindikasikan alokasi belanja yang cukup tinggi bulan ini." if er > 0.8 else "Angka ini menunjukkan kontrol belanja yang sehat."
114
 
115
  if not u_bal.empty:
116
- report += f"\n* **Analisis Saldo**: Saldo rata-rata Anda saat ini Rp{u_bal.iloc[-1]['avg_balance']:,.0f}. "
117
- report += "Tren saldo yang menurun menandakan pentingnya menjaga cadangan dana darurat." if len(u_bal) > 1 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[-2]['avg_balance'] else "Stabilitas saldo terpantau positif dan tumbuh."
118
-
119
- report += f"\n\n<div class='nbo-box'>**REKOMENDASI UNTUK ANDA: {action}**\n\n"
120
- report += f"**Tujuan:** {desc}\n\n"
121
- report += f"**Langkah-Langkah yang Dapat Anda Ambil:**\n"
122
- for i, step in enumerate(steps, 1): report += f"{i}. {step}\n"
123
- report += f"</div>"
124
- return report
125
 
126
  def create_viz(self, u_bal, u_txn):
127
- # FASE 6: VISUALIZATION [cite: 315]
128
- # 1. Inflow vs Outflow
129
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
130
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
131
  f1 = go.Figure()
@@ -133,72 +121,67 @@ class ArchonNagariEngine:
133
  f1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran (Outflow)', marker_color='#05A4DE'))
134
  f1.update_layout(title="Arus Kas Bulanan", barmode='group', template='plotly_white')
135
 
136
- # 2. Tren Saldo
137
- f2 = go.Figure()
138
- f2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#F7BD87', width=4)))
139
- f2.update_layout(title="Grafik Tren Kesehatan Saldo", template='plotly_white')
140
-
141
- # 3. Komposisi Gaya Hidup (FIXED COLORS)
142
  exp_dist = u_txn[u_txn['transaction_type'] == 'debit'].groupby('expense_type')['amount'].sum().reset_index()
143
- # Biru=Essential, Emas=Discretionary
144
  color_map = {'essential': '#05A4DE', 'discretionary': '#F7BD87'}
145
- f3 = go.Figure(data=[go.Pie(
146
- labels=exp_dist['expense_type'],
147
- values=exp_dist['amount'],
148
- hole=.4,
149
  marker=dict(colors=[color_map.get(x, '#E0EDF4') for x in exp_dist['expense_type']])
150
  )])
151
- f3.update_layout(title="Komposisi Pengeluaran (Kebutuhan vs Keinginan)")
 
 
 
 
 
152
 
153
  return f1, f2, f3
154
 
155
- # --- UI EXECUTION ---
156
- engine = ArchonNagariEngine()
157
 
158
- def run_archon(cust_id):
159
  res = engine.analyze(cust_id)
160
- if not res: return "ID Tidak Valid", "Mohon masukkan ID C0001 - C0120", None, None, None
161
 
162
- risk_lv, score, er, u_bal, u_txn, exp, inc, action, desc, steps, er_s, bt_s, od_s, mp_s = res
163
- report = engine.build_report(risk_lv, score, er, u_bal, exp, inc, action, desc, steps, cust_id, u_txn)
164
  p1, p2, p3 = engine.create_viz(u_bal, u_txn)
165
 
166
  color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
167
- sidebar_html = f"""
168
  <div class='card-sidebar'>
169
- <h2 style='color: #05A4DE; margin:0;'>Status Keuangan</h2>
170
- <div style='background:{color}; color:white; padding:10px 20px; border-radius:30px; font-weight:bold; display:inline-block; margin:15px 0;'>{risk_lv} RISK LEVEL</div>
171
- <div class='health-badge'><b>Indeks Risiko:</b> {score:.2f} / 1.00</div>
172
- <div class='health-badge'><b>Efisiensi Belanja:</b> {er:.1%} {'⚠️' if er > 0.8 else '✔️'}</div>
173
- <div class='health-badge'><b>Kesehatan Saldo:</b> {'🔻 Menurun' if bt_s == 1 else '🔺 Stabil'}</div>
174
  </div>
175
  """
176
- return sidebar_html, report, p1, p2, p3
177
 
178
  with gr.Blocks(css=custom_css) as demo:
179
  gr.HTML("<div class='nagari-header'><h1>ARCHON-AI</h1></div>")
180
  with gr.Row():
181
  with gr.Column(scale=1):
182
  id_in = gr.Textbox(label="Customer ID", placeholder="C0001")
183
- btn = gr.Button("ANALISIS DATA ANDA", variant="primary")
184
  out_side = gr.HTML()
185
  with gr.Column(scale=2):
186
  with gr.Tabs():
187
- with gr.Tab("Ringkasan Finansial"):
188
  out_report = gr.Markdown(elem_classes="report-card")
189
- with gr.Tab("Visualisasi Perilaku"):
190
  gr.Markdown("### 1. Inflow vs Outflow")
191
  plot_cf = gr.Plot()
192
- gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Interpretasi:</b> Batang muda (Inflow) idealnya lebih tinggi dari batang tua (Outflow) untuk pertumbuhan saldo yang sehat.</div>")
193
  gr.Markdown("---")
194
- gr.Markdown("### 2. Tren Pertumbuhan Saldo")
195
- plot_bal = gr.Plot()
196
- gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Interpretasi:</b> Menunjukkan daya tahan keuangan Bapak/Ibu dalam menghadapi kebutuhan mendadak.</div>")
197
- gr.Markdown("---")
198
- gr.Markdown("### 3. Komposisi Pengeluaran")
199
  plot_dist = gr.Plot()
200
- gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Warna:</b> Biru = Kebutuhan Pokok (Essential). Emas = Gaya Hidup/Keinginan (Discretionary).</div>")
 
 
 
201
 
202
- btn.click(fn=run_archon, inputs=id_in, outputs=[out_side, out_report, plot_cf, plot_bal, plot_dist])
203
 
204
  demo.launch()
 
9
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
10
  client_ai = genai.Client(api_key=GOOGLE_API_KEY) if GOOGLE_API_KEY else None
11
 
12
+ # --- UI STYLE: BANK NAGARI PREMIUM ---
 
13
  custom_css = """
14
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
15
  body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
 
19
  padding: 35px; border-radius: 15px; border-bottom: 6px solid #F7BD87;
20
  margin-bottom: 25px; text-align: center;
21
  }
22
+ .nagari-header h1 { color: #FFFFFF !important; font-weight: 800 !important; margin: 0; font-size: 2.2em; text-shadow: 2px 2px 4px rgba(0,0,0,0.2); }
23
 
24
  .card-sidebar {
25
  background: #E0EDF4; border-radius: 15px; padding: 25px;
 
27
  }
28
  .health-badge { background: white; padding: 12px; border-radius: 8px; margin-bottom: 12px; border-left: 5px solid #05A4DE; font-size: 0.95em; }
29
 
30
+ .report-card { background: white; border-radius: 12px; padding: 30px; border: 1px solid #E2E8F0; line-height: 1.8; color: #1e293b; }
 
 
 
31
  .nbo-box { background: #fffdf0; border: 2px solid #F7BD87; padding: 20px; border-radius: 10px; margin-top: 20px; }
32
  """
33
 
34
+ class ArchonMasterEngine:
35
  def __init__(self):
36
  self.load_data()
37
 
38
  def load_data(self):
 
39
  try:
40
  self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).sort_values('date')
41
  self.df_cust = pd.read_csv('customers.csv')
 
53
  if u_txn.empty or u_info_df.empty: return None
54
  u_info = u_info_df.iloc[0]
55
 
56
+ # --- FASE 2: INTELLIGENCE (Keyword Mapping) ---
57
+ # Memisahkan Kebutuhan (Essential) vs Gaya Hidup (Discretionary)
58
+ essential_keywords = ['indomaret', 'alfamart', 'pdam', 'listrik', 'cicilan', 'pinjaman', 'bill', 'gaji', 'atm', 'grocer', 'sekolah', 'rs ', 'obat']
59
 
60
  def classify_exp(row):
 
 
61
  desc = str(row.get('raw_description', '')).lower()
62
+ if any(k in desc for k in essential_keywords): return 'essential'
 
63
  return 'discretionary'
64
 
65
  u_txn['expense_type'] = u_txn.apply(classify_exp, axis=1)
66
 
67
+ # --- FASE 3 & 4: RISK SCORING (STRICT 30/20/20/20/10) ---
68
+ inc_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
69
+ ref_inc = max(inc_txn, u_info['monthly_income'])
70
+ exp_total = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
71
+ er = exp_total / ref_inc if ref_inc > 0 else 1.0
72
 
73
+ er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
74
+ bt_s = 1.0 if len(u_bal) >= 2 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[-2]['avg_balance'] else 0.0
75
+ od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
76
+ mp_s = 1.0 if (u_rep['status'] == 'late').any() else 0.0
 
77
 
78
+ score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.05
79
  risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")
80
 
81
+ # --- FASE 5: NBO ENGINE (Customer Oriented) ---
82
+ if risk_lv == "HIGH" or mp_s == 1:
83
+ action = "Evaluasi Cicilan & Restrukturisasi"
84
+ nbo_msg = "Untuk membantu meringankan beban finansial, Bapak/Ibu dapat mengajukan perpanjangan tenor atau penyesuaian jadwal pembayaran cicilan."
85
  elif er > 0.6:
86
+ action = "Pengaturan Limit Belanja (Spending Control)"
87
+ nbo_msg = "Kami mendeteksi pengeluaran bulanan yang cukup tinggi. Bapak/Ibu disarankan mengaktifkan fitur limit transaksi QRIS untuk menjaga tabungan."
88
  elif risk_lv == "LOW":
89
+ action = "Pengembangan Aset (Saving/Investment)"
90
+ nbo_msg = "Kondisi keuangan Bapak/Ibu sangat prima. Ini waktu yang tepat untuk menanamkan dana mengendap ke Deposito atau Tabungan Berjangka."
91
  else:
92
+ action = "Edukasi Pengelolaan Kas"
93
+ nbo_msg = "Mari optimalkan arus kas harian Bapak/Ibu dengan fitur budgeting di aplikasi mobile kami agar resiliensi tetap terjaga."
94
 
95
+ return risk_lv, score, er, u_bal, u_txn, exp_total, ref_inc, action, nbo_msg, er_s, bt_s, od_s, mp_s
96
 
97
+ def get_report(self, risk_lv, score, er, u_bal, exp, inc, action, nbo_msg, cid, u_txn):
98
+ # FASE 6: EXPLAINABLE NARRATIVE
99
+ txt = f"### ANALISIS RESILIENSI FINANSIAL: {risk_lv}\n\n"
100
+ txt += f"Halo Bapak/Ibu, sistem Archon telah meninjau riwayat transaksi Anda ({cid}) dengan hasil sebagai berikut:\n\n"
101
 
102
+ txt += f"**1. Mengapa Level Risiko Saya {risk_lv}?**\n"
103
+ txt += f"* **Rasio Belanja ({er:.1%})**: Anda menghabiskan Rp{exp:,.0f} dari pendapatan Rp{inc:,.0f}. "
104
+ txt += "Rasio di atas 80% menunjukkan bahwa hampir seluruh pendapatan terpakai habis, yang berisiko jika ada kebutuhan darurat." if er > 0.8 else "Angka ini menunjukkan kontrol belanja yang sangat sehat."
105
 
106
  if not u_bal.empty:
107
+ txt += f"\n* **Analisis Saldo**: Saldo rata-rata Anda Rp{u_bal.iloc[-1]['avg_balance']:,.0f}. "
108
+ txt += "Terdeteksi tren saldo menurun, yang berarti Bapak/Ibu mulai menggunakan dana cadangan untuk biaya hidup harian." if len(u_bal) > 1 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[-2]['avg_balance'] else "Saldo Anda tumbuh dengan stabil dan aman."
109
+
110
+ txt += f"\n\n<div class='nbo-box'>REKOMENDASI UNTUK ANDA: {action}\n\n"
111
+ txt += f"**Saran Kami:** {nbo_msg}\n\n"
112
+ txt += f"**Tujuan:** Agar resiliensi (daya tahan) keuangan Bapak/Ibu tetap kuat menghadapi fluktuasi ekonomi di masa depan.</div>"
113
+ return txt
 
 
114
 
115
  def create_viz(self, u_bal, u_txn):
116
+ # 1. Arus Kas
 
117
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
118
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
119
  f1 = go.Figure()
 
121
  f1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran (Outflow)', marker_color='#05A4DE'))
122
  f1.update_layout(title="Arus Kas Bulanan", barmode='group', template='plotly_white')
123
 
124
+ # 2. Pie Chart (FIXED COLORS)
 
 
 
 
 
125
  exp_dist = u_txn[u_txn['transaction_type'] == 'debit'].groupby('expense_type')['amount'].sum().reset_index()
 
126
  color_map = {'essential': '#05A4DE', 'discretionary': '#F7BD87'}
127
+ f2 = go.Figure(data=[go.Pie(
128
+ labels=exp_dist['expense_type'], values=exp_dist['amount'], hole=.4,
 
 
129
  marker=dict(colors=[color_map.get(x, '#E0EDF4') for x in exp_dist['expense_type']])
130
  )])
131
+ f2.update_layout(title="Komposisi Pengeluaran (Kebutuhan vs Gaya Hidup)")
132
+
133
+ # 3. Tren Saldo
134
+ f3 = go.Figure()
135
+ f3.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#F7BD87', width=4)))
136
+ f3.update_layout(title="Tren Pertumbuhan Saldo", template='plotly_white')
137
 
138
  return f1, f2, f3
139
 
140
+ # --- UI LOGIC ---
141
+ engine = ArchonMasterEngine()
142
 
143
+ def process(cust_id):
144
  res = engine.analyze(cust_id)
145
+ if not res: return "ID Tidak Valid", "Gunakan C0001 - C0120", None, None, None
146
 
147
+ risk_lv, score, er, u_bal, u_txn, exp, inc, action, nbo_msg, er_s, bt_s, od_s, mp_s = res
148
+ report = engine.get_report(risk_lv, score, er, u_bal, exp, inc, action, nbo_msg, cust_id, u_txn)
149
  p1, p2, p3 = engine.create_viz(u_bal, u_txn)
150
 
151
  color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
152
+ sidebar = f"""
153
  <div class='card-sidebar'>
154
+ <h2 style='color: #05A4DE; margin:0;'>Ringkasan AI</h2>
155
+ <div style='background:{color}; color:white; padding:10px 20px; border-radius:30px; font-weight:bold; display:inline-block; margin:15px 0;'>{risk_lv} RISK</div>
156
+ <div class='health-badge'><b>Skor Risiko:</b> {score:.2f} / 1.00</div>
157
+ <div class='health-badge'><b>Indikator Saldo:</b> {'🔻 Menurun' if bt_s == 1 else '🔺 Stabil'}</div>
158
+ <div class='health-badge'><b>Gaya Hidup:</b> {'⚠️ Boros' if er > 0.8 else '✔️ Aman'}</div>
159
  </div>
160
  """
161
+ return sidebar, report, p1, p2, p3
162
 
163
  with gr.Blocks(css=custom_css) as demo:
164
  gr.HTML("<div class='nagari-header'><h1>ARCHON-AI</h1></div>")
165
  with gr.Row():
166
  with gr.Column(scale=1):
167
  id_in = gr.Textbox(label="Customer ID", placeholder="C0001")
168
+ btn = gr.Button("ANALYZE CUSTOMER", variant="primary")
169
  out_side = gr.HTML()
170
  with gr.Column(scale=2):
171
  with gr.Tabs():
172
+ with gr.Tab("Laporan Naratif"):
173
  out_report = gr.Markdown(elem_classes="report-card")
174
+ with gr.Tab("Analisis Visual"):
175
  gr.Markdown("### 1. Inflow vs Outflow")
176
  plot_cf = gr.Plot()
 
177
  gr.Markdown("---")
178
+ gr.Markdown("### 2. Komposisi Gaya Hidup")
 
 
 
 
179
  plot_dist = gr.Plot()
180
+ gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Keterangan:</b> Biru = Kebutuhan Pokok (Essential). Emas = Gaya Hidup (Discretionary).</div>")
181
+ gr.Markdown("---")
182
+ gr.Markdown("### 3. Tren Pertumbuhan Saldo")
183
+ plot_bal = gr.Plot()
184
 
185
+ btn.click(fn=process, inputs=id_in, outputs=[out_side, out_report, plot_cf, plot_dist, plot_bal])
186
 
187
  demo.launch()