1na37 commited on
Commit
c43aa13
·
verified ·
1 Parent(s): 710c763

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +610 -700
app.py CHANGED
@@ -3,12 +3,13 @@
3
  # Final Project | AI Engineering Bootcamp Batch 10
4
  # Author: 1na37
5
  # ============================================================
6
- # FIX: Media injection menggunakan window._sme (state persistent)
7
- # FIX: SHAP image sebagai base64 agar tidak expired
8
- # FIX: Progress bar loading tab real-time
9
- # FIX: Kode mati dihapus
10
- # FIX v2: MAYA_TOOLS missing closing } before ]
11
- # FIX v2: JS Python comment # inside string → removed
 
12
  # ============================================================
13
 
14
  import streamlit as st
@@ -17,6 +18,7 @@ import numpy as np
17
  import matplotlib
18
  matplotlib.use('Agg')
19
  import matplotlib.pyplot as plt
 
20
  import base64
21
  import time as _time
22
  import shap, joblib, os, json, warnings, re, requests, io, hashlib
@@ -30,7 +32,7 @@ st.set_page_config(
30
  )
31
 
32
  # ============================================================
33
- # TRANSLATION SYSTEM — Latin Hindi (Hinglish transliteration)
34
  # ============================================================
35
  TRANSLATIONS = {
36
  'sidebar_api': {'id':'🔑 API Keys (opsional)', 'en':'🔑 API Keys (optional)', 'hi':'🔑 API Keys (vaikalpik)'},
@@ -129,7 +131,9 @@ TRANSLATIONS = {
129
  'wi_pd_from': {'id':'dari', 'en':'from', 'hi':'se'},
130
  'wi_no_change': {'id':'Perubahan minimal pada skor risiko','en':'Minimal change in risk score','hi':'Jokhim score mein nyoonatam badlaav'},
131
  'wi_tips_title': {'id':'💡 Tips Optimasi Skor', 'en':'💡 Score Optimization Tips', 'hi':'💡 Score Sudhaar Tips'},
132
- 'wi_form_first': {'id':'Submit form terlebih dahulu', 'en':'Submit the form first', 'hi':'Pehle form submit karein'},
 
 
133
  'wi_approved': {'id':'LAYAK', 'en':'APPROVED', 'hi':'SWIKAARY'},
134
  'wi_review': {'id':'REVIEW', 'en':'REVIEW', 'hi':'SAMEEKSHA'},
135
  'wi_highrisk': {'id':'RISIKO TINGGI', 'en':'HIGH RISK', 'hi':'UCHCH JOKHIM'},
@@ -153,10 +157,6 @@ TRANSLATIONS = {
153
  'risk_approved': {'id':'🟢 LAYAK', 'en':'🟢 APPROVED', 'hi':'🟢 YOGYA'},
154
  'risk_review': {'id':'🟡 PERLU REVIEW', 'en':'🟡 REVIEW', 'hi':'🟡 SAMEEKSHA'},
155
  'risk_high': {'id':'🔴 BERISIKO TINGGI', 'en':'🔴 HIGH RISK', 'hi':'🔴 UCHCH JOKHIM'},
156
- 'empty_ensemble': {'id':'Ensemble', 'en':'Ensemble', 'hi':'Ensemble'},
157
- 'empty_xai': {'id':'XAI', 'en':'XAI', 'hi':'XAI'},
158
- 'empty_narrative': {'id':'Narasi', 'en':'Narrative', 'hi':'Vivarana'},
159
- 'empty_chat': {'id':'AI Chat', 'en':'AI Chat', 'hi':'AI Baatcheet'},
160
  }
161
 
162
  def T(key: str, lang: str) -> str:
@@ -164,7 +164,7 @@ def T(key: str, lang: str) -> str:
164
  return entry.get(lang, entry.get('en', key))
165
 
166
  # ============================================================
167
- # SELECTBOX LABELS — Latin Hindi transliteration
168
  # ============================================================
169
  CHECKING_OPTS = ['<0', '0<=X<200', '>=200', 'no checking']
170
  CHECKING_LABELS = {
@@ -220,7 +220,7 @@ HOUSING_LABELS = {
220
  LANG_LABELS = {'id':'🇮🇩 Bahasa Indonesia','en':'🇬🇧 English','hi':'🇮🇳 Hindi (Roman)'}
221
 
222
  # ============================================================
223
- # CSS STYLING
224
  # ============================================================
225
  st.markdown("""
226
  <style>
@@ -295,7 +295,7 @@ def _log_api(step: str, src: str, ok: bool, ms: int):
295
  st.session_state.api_log = st.session_state.api_log[-20:]
296
 
297
  # ============================================================
298
- # _set_status
299
  # ============================================================
300
  def _set_status(msg: str):
301
  st.session_state['llm_status'] = msg
@@ -313,8 +313,7 @@ def _set_status(msg: str):
313
  pass
314
 
315
  # ============================================================
316
- # FORMAL TOOL CALLING SCHEMA (Agentic AI)
317
- # FIX: Added missing closing } before ] on MAYA_TOOLS
318
  # ============================================================
319
  MAYA_TOOLS = [
320
  {
@@ -340,10 +339,9 @@ MAYA_TOOLS = [
340
  "required": ["field", "value", "reason"]
341
  }
342
  }
343
- } # <-- FIX: was missing closing } here; only had ]
344
  ]
345
 
346
- # Tool-capable model subsets
347
  _OR_TOOL_MODELS = [
348
  "google/gemini-2.0-flash-exp:free",
349
  "qwen/qwen3-32b:free",
@@ -358,7 +356,7 @@ _GROQ_TOOL_MODELS = [
358
  ]
359
 
360
  # ============================================================
361
- # LLM API CALLERS (with status + timing)
362
  # ============================================================
363
  def call_openrouter_tools(messages, api_key, tools=None):
364
  if not api_key:
@@ -485,7 +483,6 @@ def _clean_response(text: str) -> tuple:
485
  return text, adjustments
486
 
487
  def _call_chat_llm(messages):
488
- """Cascade: OR tools → Groq tools → OR no-tools → Groq no-tools."""
489
  _or = st.session_state.get("openrouter_key", "")
490
  _grq = st.session_state.get("groq_key", "")
491
  if _or:
@@ -587,9 +584,6 @@ def _load_chat_memory():
587
  pass
588
  return [], ""
589
 
590
- # ============================================================
591
- # MEMORY SUMMARIZATION
592
- # ============================================================
593
  def _summarize_history(history, lang):
594
  if len(history) <= 8:
595
  return history, st.session_state.get('chat_summary', '')
@@ -616,7 +610,7 @@ def _summarize_history(history, lang):
616
  st.session_state.chat_summary = new_summary
617
  return recent_turns, new_summary
618
 
619
- # Early init
620
  for _k, _v in [('gif_muted', False), ('llm_status', ''), ('api_log', []), ('_last_muted_sent', None)]:
621
  if _k not in st.session_state:
622
  st.session_state[_k] = _v
@@ -628,6 +622,7 @@ with st.sidebar:
628
  st.markdown("## 🏦 SME Credit Risk AI")
629
  st.markdown("---")
630
 
 
631
  if 'lang_sel' not in st.session_state:
632
  st.session_state.lang_sel = 'id'
633
  lang = st.radio(
@@ -638,7 +633,8 @@ with st.sidebar:
638
  )
639
  st.markdown("---")
640
 
641
- _musik_labels = {'id': '### 🎵 Musik', 'en': '### 🎵 Music', 'hi': '### 🎵 Sangeet'}
 
642
  st.markdown(_musik_labels.get(lang, _musik_labels['id']))
643
 
644
  _mute_labels = {
@@ -648,7 +644,7 @@ with st.sidebar:
648
  }
649
  _ml = _mute_labels.get(lang, _mute_labels['id'])
650
  mute_label = _ml[0] if not st.session_state.gif_muted else _ml[1]
651
- if st.button(mute_label, key="btn_mute", width='stretch'):
652
  st.session_state.gif_muted = not st.session_state.gif_muted
653
  st.rerun()
654
 
@@ -659,6 +655,7 @@ with st.sidebar:
659
  }
660
  st.caption(_musik_active.get(lang, _musik_active['id']))
661
 
 
662
  _media_files_map = {
663
  'crying_cat.gif': {'id':'GIF Kucing Nangis (tab SHAP)','en':'Crying Cat GIF (SHAP tab)','hi':'Rota Billa GIF (SHAP tab)'},
664
  'dance1.gif': {'id':'Dance GIF 1 (What-If)','en':'Dance GIF 1 (What-If)','hi':'Dance GIF 1 (What-If)'},
@@ -669,18 +666,14 @@ with st.sidebar:
669
  'idk.mp3': {'id':'Musik SHAP (idk.mp3)', 'en':'SHAP Music (idk.mp3)', 'hi':'SHAP Sangeet (idk.mp3)'},
670
  'shake.mp3': {'id':'Musik What-If (shake.mp3)','en':'What-If Music (shake.mp3)','hi':'What-If Sangeet (shake.mp3)'},
671
  }
672
- _missing = [(fname, labels[lang]) for fname, labels in _media_files_map.items()
673
- if not os.path.exists('static/' + fname)]
674
- _found = [(fname, labels[lang]) for fname, labels in _media_files_map.items()
675
- if os.path.exists('static/' + fname)]
676
  _total = len(_media_files_map)
677
-
678
  _upload_hint = {
679
- 'id': 'Upload file ke folder `static/` di HF Space untuk mengaktifkan GIF & musik.',
680
- 'en': 'Upload files to the `static/` folder in your HF Space to enable GIFs & music.',
681
- 'hi': '`static/` folder mein files upload karein HF Space par GIF & sangeet ke liye.',
682
  }
683
-
684
  if _missing:
685
  _warn_title = {
686
  'id': '⚠️ Media belum diupload (' + str(len(_missing)) + '/' + str(_total) + ' hilang)',
@@ -693,26 +686,21 @@ with st.sidebar:
693
  st.caption(_upload_hint.get(lang, _upload_hint['id']))
694
  if _found:
695
  _partial = {
696
- 'id': '⏳ ' + str(len(_found)) + '/' + str(_total) + ' media siap — GIF/musik sebagian aktif',
697
- 'en': '⏳ ' + str(len(_found)) + '/' + str(_total) + ' media ready — GIF/music partially active',
698
- 'hi': '⏳ ' + str(len(_found)) + '/' + str(_total) + ' media taiyaar — GIF/sangeet aadha active',
699
  }
700
  st.caption(_partial.get(lang, _partial['id']))
701
  else:
702
  _all_ok = {
703
- 'id': '✅ Semua media ditemukan (' + str(_total) + ' file) — GIF & musik aktif saat buka tab',
704
- 'en': '✅ All media found (' + str(_total) + ' files) — GIFs & music activate on tab open',
705
- 'hi': '✅ Sabhi media mila (' + str(_total) + ' files)Tab kholne par GIF & sangeet active hoga',
706
  }
707
  st.caption(_all_ok.get(lang, _all_ok['id']))
708
- _stage_label = {
709
- 'id': '📂 File ditemukan → 🖼️ GIF dimuat ke memori → 🚀 Siap tampil saat ganti tab',
710
- 'en': '📂 Files found → 🖼️ GIFs loaded to memory → 🚀 Ready to appear on tab switch',
711
- 'hi': '📂 Files mile → 🖼️ GIF memory mein load hue → 🚀 Tab switch par dikhenge',
712
- }
713
- st.caption(_stage_label.get(lang, _stage_label['id']))
714
  st.markdown("---")
715
 
 
716
  openrouter_key = st.session_state.openrouter_key
717
  groq_key = st.session_state.groq_key
718
  st.caption("🔑 **OpenRouter:** " + ('✅' if openrouter_key else '❌') + " **Groq:** " + ('✅' if groq_key else '❌'))
@@ -728,6 +716,7 @@ with st.sidebar:
728
  pass
729
  st.markdown("---")
730
 
 
731
  st.markdown("### 📡 API Tracker")
732
  _active_status = st.session_state.get('llm_status', '')
733
  if _active_status:
@@ -743,11 +732,12 @@ with st.sidebar:
743
  st.caption(icon + " `" + entry['step'] + "` — " + entry['src'] + " (" + str(entry['ms']) + "ms)")
744
  else:
745
  st.caption("_Belum ada aktivitas API_")
746
- if st.button("🗑️ Reset Log", key="reset_api_log", width='stretch'):
747
  st.session_state.api_log = []
748
  st.rerun()
749
  st.markdown("---")
750
 
 
751
  st.markdown(T('sidebar_stack', lang))
752
  stack_items = {
753
  'id': "- 🤖 XGBoost + LightGBM + RF\n- 🔍 SHAP Eksplainabilitas\n- 💬 Narasi AI (OpenRouter + Groq)\n- 🤖 Maya AI Chat + Tool Calling\n- 🧠 Chain-of-Thought + Few-shot\n- 💾 Memory Summarization\n- 🌍 ID / EN / HI",
@@ -861,7 +851,6 @@ def shap_summary(vals, names, n=5):
861
  ])
862
 
863
  def make_shap_png(shap_vals, feature_names):
864
- """Returns base64 data URI — avoids Streamlit MediaFileStorage expiry."""
865
  sv = np.array(shap_vals)
866
  names = list(feature_names)
867
  n = min(12, len(sv))
@@ -883,7 +872,7 @@ def make_shap_png(shap_vals, feature_names):
883
  return 'data:image/png;base64,' + b64
884
 
885
  # ============================================================
886
- # LLM HELPERS — Narrative cascade
887
  # ============================================================
888
  _OR_FREE_MODELS = [
889
  "google/gemini-2.0-flash-exp:free",
@@ -931,13 +920,11 @@ def call_openrouter(messages, api_key, model=None):
931
  except requests.exceptions.Timeout:
932
  ms = int((_time.time() - t0) * 1000)
933
  last_error = "Timeout (" + m + ")"
934
- short = m.split('/')[-1].replace(':free','')
935
- _log_api("Narasi", "OR/" + short, False, ms)
936
  except Exception as e:
937
  ms = int((_time.time() - t0) * 1000)
938
  last_error = m + ": " + str(e)[:100]
939
- short = m.split('/')[-1].replace(':free','')
940
- _log_api("Narasi", "OR/" + short, False, ms)
941
  return None, last_error
942
 
943
  def call_groq(messages, api_key):
@@ -973,20 +960,17 @@ def call_groq(messages, api_key):
973
  return None, last_error
974
 
975
  def _call_llm(messages):
976
- """Narrative LLM: OpenRouter free → Groq free → None."""
977
  _or = st.session_state.get("openrouter_key", "")
978
  _grq = st.session_state.get("groq_key", "")
979
  last_err = None
980
  if _or:
981
  r, last_err = call_openrouter(messages, _or)
982
  if r:
983
- model_used = _OR_FREE_MODELS[0].split("/")[-1].replace(":free","")
984
- return r, 'OpenRouter (' + model_used + ')', None
985
  if _grq:
986
  r, last_err = call_groq(messages, _grq)
987
  if r:
988
- model_used = _GROQ_FREE_MODELS[0]
989
- return r, 'Groq (' + model_used + ')', None
990
  return None, None, last_err or "No API key configured"
991
 
992
  # ============================================================
@@ -1128,7 +1112,7 @@ def _get_top_issue(raw_input, pd_pct, lang):
1128
  if not npwp:
1129
  return _t("prioritas utama: **urus NPWP** dulu di pajak.go.id","top priority: **register NPWP** at pajak.go.id","mukhya prathamikta: **NPWP register karein** pajak.go.id par")
1130
  elif dig < 40:
1131
- return _t("**digital score " + str(dig) + "/100** adalah area paling kritis untuk ditingkatkan","**digital score " + str(dig) + "/100** is the most critical area to improve","**digital score " + str(dig) + "/100** sabse mahatvapurn sudhaar kshetra hai")
1132
  elif cf < 10e6:
1133
  return _t("**cash flow Rp " + str(int(cf/1e6)) + "jt/bln** perlu dioptimalkan","**cash flow Rp " + str(int(cf/1e6)) + "M/month** needs optimization","**naqad pravaah Rp " + str(int(cf/1e6)) + "M/maah** optimize zaroori hai")
1134
  elif biz < 2:
@@ -1136,7 +1120,7 @@ def _get_top_issue(raw_input, pd_pct, lang):
1136
  elif pd_pct < 20:
1137
  return _t("profil kamu sudah sangat bagus!","your profile is already excellent!","aapka parichay pehle se behtareen hai!")
1138
  else:
1139
- return _t("beberapa area bisa diperbaiki — tanya aku lebih spesifik!","several areas can be improved — ask me specifically!","kai kshetra sudhaare jaa sakte hain — vishesh roop se puchein!")
1140
 
1141
  # ============================================================
1142
  # AI CHAT — Maya persona
@@ -1206,28 +1190,20 @@ CARA BERPIKIR — CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
1206
  "- Pinjaman: Rp " + str(int(raw_input.get('loan_rp',50e6)/1e6)) + "M | Tenor: " + str(raw_input.get('duration',24)) + " bulan\n"
1207
  "- Riwayat Kredit: " + str(raw_input.get('credit_history')) + "\n\n"
1208
  "FAKTOR RISIKO UTAMA (SHAP):\n" + factors + "\n"
1209
- + context
1210
- + summary_block
1211
- + cot_block
1212
- + fewshot
1213
- + "ATURAN MENJAWAB — BACA SEMUA DENGAN SEKSAMA:\n"
1214
- "1. Jawab BEBAS — tidak harus soal kredit. Kalau ditanya soal diri sendiri, perkenalkan sebagai Maya.\n"
1215
  "2. Kalau relevan, selalu hubungkan ke data pemohon dengan menyebut angka aktualnya.\n"
1216
  "3. Berikan saran KONKRET dan SPESIFIK, bukan generik.\n"
1217
  "4. Maksimal 150 kata kecuali diminta lebih panjang.\n"
1218
- "5. FORMAT TAG [ADJUST:] — WAJIB IKUTI ATURAN INI:\n"
1219
- " BENAR: Embed tag langsung di akhir kalimat saran:\n"
1220
- " 'Coba naikkan digital score ke 75 [ADJUST: digital_presence_score=75] dampaknya paling besar.'\n"
1221
- " SALAH: Menulis kalimat saran TANPA tag, lalu menambah baris terpisah\n"
1222
- " SALAH: Menulis tag di baris sendiri di akhir response\n"
1223
- " SALAH BESAR: Menulis ulang rekomendasi di baris terpisah setelah selesai\n"
1224
- " RULE: Setiap rekomendasi bernilai angka = SATU kalimat + SATU tag [ADJUST:] inline. TIDAK LEBIH.\n"
1225
- " RULE: JANGAN pernah tulis JSON object {...} di response.\n"
1226
- " RULE: Field valid: digital_presence_score(1-100), business_age_years(1-20),\n"
1227
  " num_employees(1-50), monthly_cash_flow(angka Rp), duration(4-72), loan_rp(angka Rp)\n"
1228
  "6. JANGAN pernah balik ke template. Jawab seperti manusia cerdas.\n"
1229
  "7. Baca context percakapan sebelumnya sebelum menjawab — jaga konsistensi topik.\n"
1230
- "8. AKHIRI response dengan kalimat lengkap. Jangan tambahkan apapun setelah kalimat terakhir.\n"
1231
  )
1232
 
1233
  messages = [{"role": "system", "content": system}]
@@ -1240,6 +1216,7 @@ CARA BERPIKIR — CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
1240
  if not response and tool_calls:
1241
  response = _tool_call_to_text(tool_calls, lang)
1242
 
 
1243
  if response is None:
1244
  low = user_msg.lower()
1245
  dig = raw_input.get('digital_presence_score', 0)
@@ -1252,90 +1229,26 @@ CARA BERPIKIR — CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
1252
 
1253
  if any(w in low for w in ['siapa','kamu','km','who are you','nama','you are','maya','halo','hai','hei','hello','hi','perkenalan']):
1254
  response = _t(
1255
- "Hei! Aku **Maya**, AI Credit Advisor kamu. Aku dirancang untuk bantu kamu pahami skor kredit dan strategi bisnis UMKM. "
1256
- "Skor PD kamu sekarang **" + str(round(pd_pct,1)) + "%** — " + ('sudah bagus banget!' if pd_pct < 20 else 'masih ada ruang untuk diperbaiki.') + " "
1257
- "Mau aku jelasin lebih detail atau ada yang mau ditanyain?",
1258
- "Hey! I'm **Maya**, your AI Credit Advisor. I help you understand your credit score and SME business strategy. "
1259
- "Your PD score is **" + str(round(pd_pct,1)) + "%** — " + ('looking great!' if pd_pct < 20 else 'there is room to improve.') + " "
1260
- "Anything you want to ask?",
1261
- "Namaste! Main **Maya** hoon, aapki AI Credit Advisor. "
1262
- "Aapka PD score **" + str(round(pd_pct,1)) + "%** hai — " + ('bahut badhiya!' if pd_pct < 20 else 'sudhaar ki gunjaish hai.') + " "
1263
- "Kuch poochna hai?"
1264
- )
1265
- elif any(w in low for w in ['cuan','profit','untung','laba','cepet cuan','penghasilan cepat','cara cepet','duit cepet']):
1266
- response = _t(
1267
- "Cuan cepet? Sah-sah aja! Tapi di kredit, yang bikin bank percaya bukan seberapa cepat, tapi **konsistensi**. "
1268
- "Dari profil kamu (cash flow Rp " + str(int(cf/1e6)) + "jt/bln, digital " + str(dig) + "/100):\n\n"
1269
- "1. **Aktifin marketplace** → cash flow naik [ADJUST: monthly_cash_flow=25000000]\n"
1270
- "2. **Google Business aktif** → digital score naik [ADJUST: digital_presence_score=75]\n"
1271
- "3. **Dokumentasikan semua pemasukan** — pisah rekening bisnis & pribadi",
1272
- "Quick profit? Totally valid! But in credit, **consistency** matters more. "
1273
- "From your profile (CF Rp " + str(int(cf/1e6)) + "M/mo, digital " + str(dig) + "/100):\n\n"
1274
- "1. Active marketplace → CF up [ADJUST: monthly_cash_flow=25000000]\n"
1275
- "2. Google Business → digital up [ADJUST: digital_presence_score=75]\n"
1276
- "3. Document all income streams",
1277
- "Jaldi munafa? **Niyamitata** zaroori hai.\n"
1278
- "1) Marketplace active karein [ADJUST: monthly_cash_flow=25000000] "
1279
- "2) Google Business [ADJUST: digital_presence_score=75] 3) Aay document karein"
1280
  )
1281
- adjustments['digital_presence_score'] = 75
1282
- adjustments['monthly_cash_flow'] = 25000000
1283
- elif any(w in low for w in ['improve','better','lower','reduce','tingkatkan','kurangi','turunkan','cara','gimana','bagaimana','naik','turun','meningkat','naikkan','optimalkan']):
1284
  tips = []
1285
  if not npwp:
1286
- tips.append(_t("1. **Urus NPWP** — wajib untuk pinjaman formal, daftar di pajak.go.id",
1287
- "1. **Register NPWP** — required for formal loans, apply at pajak.go.id",
1288
- "1. **NPWP register karein** — aupchaarik rin ke liye zaroori"))
1289
- adjustments['has_npwp'] = 1
1290
  if dig < 75:
1291
  n = '2' if not npwp else '1'
1292
- tips.append(_t(n + ". **Naikkan Digital Score dari " + str(dig) + " ke 75+** aktif di Google Business, Tokopedia/Shopee",
1293
- n + ". **Raise Digital Score from " + str(dig) + " to 75+** — Google Business, marketplace",
1294
- n + ". **Digital Score " + str(dig) + " se 75+ karein** — Google Business, marketplace"))
1295
  adjustments['digital_presence_score'] = 75
1296
  if cf < 25e6:
1297
  n = str(len(tips) + 1)
1298
- tips.append(_t(n + ". **Optimalkan cash flow dari Rp " + str(int(cf/1e6)) + "jt ke 25jt+/bln** diversifikasi produk",
1299
- n + ". **Grow cash flow from Rp " + str(int(cf/1e6)) + "M to 25M+/month**",
1300
- n + ". **Naqad pravaah Rp " + str(int(cf/1e6)) + "M se 25M+ karein**"))
1301
  adjustments['monthly_cash_flow'] = 25000000
1302
  if not tips:
1303
- tips.append(_t("Profil kamu sudah solid dengan PD " + str(round(pd_pct,1)) + "%! Coba simulasikan di tab What-If.",
1304
- "Your profile is solid at PD " + str(round(pd_pct,1)) + "%! Try the What-If tab.",
1305
- "Aapka parichay PD " + str(round(pd_pct,1)) + "% ke saath mazboot hai!"))
1306
- response = (
1307
- _t("Untuk turunkan PD dari **" + str(round(pd_pct,1)) + "%**, fokus ke:\n\n",
1308
- "To lower PD from **" + str(round(pd_pct,1)) + "%**, focus on:\n\n",
1309
- "PD **" + str(round(pd_pct,1)) + "%** kam karne ke liye:\n\n")
1310
- + '\n'.join(tips[:3])
1311
- )
1312
- elif any(w in low for w in ['why','kenapa','mengapa','factor','faktor','shap','pengaruh','driver','apa yang','jelasin','jelaskan','explain','definisi']):
1313
- ti = int(np.argsort(np.abs(shap_vals))[-1])
1314
- ti2 = int(np.argsort(np.abs(shap_vals))[-2])
1315
- fn = feature_names[ti]
1316
- if 'business_age' in fn:
1317
- analogi = _t(" — ibarat pengalaman kerja, makin lama makin dipercaya"," — like work experience, longer = more trustworthy"," — kaam ke anubhav ki tarah, zyada = zyada bharosemand")
1318
- elif 'cash_flow' in fn:
1319
- analogi = _t(" — ibarat gaji bulanan kamu, makin besar makin mudah bayar cicilan"," — like your monthly income, higher = easier to repay"," — maasik aay ki tarah, zyada = kist bhrna aasaan")
1320
- elif 'digital' in fn:
1321
- analogi = _t(" — ibarat 'nilai reputasi online' bisnis kamu di mata bank"," — your business's 'online reputation score' in the bank's eyes"," — bank ki nazar mein vyapaar ka 'online pratishtha score'")
1322
- else:
1323
- analogi = ""
1324
- response = _t(
1325
- "Dua faktor terbesar yang drive skor PD **" + str(round(pd_pct,1)) + "%** kamu:\n\n"
1326
- "1. **" + fn + "**" + analogi + "\n → " + ('meningkatkan' if shap_vals[ti] > 0 else 'menurunkan') + " risiko (SHAP=" + str(round(shap_vals[ti],3)) + ")\n\n"
1327
- "2. **" + feature_names[ti2] + "**\n → " + ('meningkatkan' if shap_vals[ti2] > 0 else 'menurunkan') + " risiko (SHAP=" + str(round(shap_vals[ti2],3)) + ")\n\n"
1328
- + ('Skor bagus! Kedua faktor ini justru mendukung kelayakan kamu.' if pd_pct < 20 else 'Fokus perbaiki faktor pertama dulu — dampaknya paling besar.'),
1329
- "Two biggest drivers of your PD **" + str(round(pd_pct,1)) + "%**:\n\n"
1330
- "1. **" + fn + "**" + analogi + "\n → " + ('increases' if shap_vals[ti] > 0 else 'decreases') + " risk (SHAP=" + str(round(shap_vals[ti],3)) + ")\n\n"
1331
- "2. **" + feature_names[ti2] + "**\n → " + ('increases' if shap_vals[ti2] > 0 else 'decreases') + " risk (SHAP=" + str(round(shap_vals[ti2],3)) + ")\n\n"
1332
- + ('Great score! Both factors support your eligibility.' if pd_pct < 20 else 'Focus on the first factor — it has the biggest impact.'),
1333
- "Aapke PD **" + str(round(pd_pct,1)) + "%** ke do mukhya kaarak:\n\n"
1334
- "1. **" + fn + "**" + analogi + "\n → " + ('badhata' if shap_vals[ti] > 0 else 'ghataata') + " jokhim (SHAP=" + str(round(shap_vals[ti],3)) + ")\n\n"
1335
- "2. **" + feature_names[ti2] + "**\n → " + ('badhata' if shap_vals[ti2] > 0 else 'ghataata') + " jokhim (SHAP=" + str(round(shap_vals[ti2],3)) + ")\n\n"
1336
- + ('Badhiya score! Dono kaarak yogyata ka samarthan karte hain.' if pd_pct < 20 else 'Pehle pehle kaarak sudhaarein — sabse bada prabhav.')
1337
- )
1338
- elif any(w in low for w in ['pinjaman','loan','kredit','berapa','amount','besar','limit','ideal','rekomendasi pinjaman']):
1339
  if cf > 0:
1340
  dur_val = raw_input.get('duration', 24)
1341
  safe = cf * dur_val * 0.35
@@ -1343,92 +1256,17 @@ CARA BERPIKIR — CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
1343
  if cur > safe:
1344
  adjustments['loan_rp'] = safe
1345
  response = _t(
1346
- "Berdasarkan cash flow kamu **Rp " + str(int(cf/1e6)) + "jt/bulan** dan tenor " + str(int(dur_val)) + " bulan, "
1347
- "pinjaman ideal maksimal **Rp " + str(int(safe/1e6)) + "jt** (rasio cicilan 35%).\n\n"
1348
- "Pinjaman kamu sekarang Rp " + str(int(cur/1e6)) + "jt — "
1349
- + ('masih aman, good job!' if cur <= safe else 'melebihi batas aman. Pertimbangkan kurangi ke Rp ' + str(int(safe/1e6)) + 'jt atau perpanjang tenor.'),
1350
- "Based on your cash flow **Rp " + str(int(cf/1e6)) + "M/month** and " + str(int(dur_val)) + "-month tenure, "
1351
- "ideal loan is max **Rp " + str(int(safe/1e6)) + "M** (35% installment ratio).\n\n"
1352
- "Current loan Rp " + str(int(cur/1e6)) + "M — "
1353
- + ('within safe limits, good job!' if cur <= safe else 'exceeds safe limit. Consider reducing to Rp ' + str(int(safe/1e6)) + 'M.'),
1354
- "Aapke naqad pravaah **Rp " + str(int(cf/1e6)) + "M/maah** aur " + str(int(dur_val)) + " maah ke aadhaar par "
1355
- "adhiktam rin **Rp " + str(int(safe/1e6)) + "M** (35% kist anupaat).\n\n"
1356
- "Vartamaan rin Rp " + str(int(cur/1e6)) + "M — " + ('surakshit!' if cur <= safe else 'seema se adhik.')
1357
  )
1358
  else:
1359
- response = _t("Isi cash flow bulanan di form dulu ya, biar aku bisa kasih rekomendasi akurat!",
1360
- "Fill in your monthly cash flow first for an accurate recommendation!",
1361
- "Sahi sujhaav ke liye pehle maasik naqad pravaah bharein!")
1362
- elif any(w in low for w in ['tabungan','saving','nabung','menabung','savings','uang','keuangan','finance','financial','simpan']):
1363
  response = _t(
1364
- "Soal tabungan dan cash flow kamu (saat ini **Rp " + str(int(cf/1e6)) + "jt/bln**):\n\n"
1365
- "3 cara praktis tingkatkan tabungan UMKM:\n"
1366
- "1. Pisahkan rekening bisnis & pribadibiar tidak tercampur\n"
1367
- "2. Sisihkan minimal 10-15% dari omzet setiap bulan secara otomatis\n"
1368
- "3. Dokumentasikan semua pemasukan — ini juga naikkan digital score!\n\n"
1369
- "Tabungan naik → cash flow lebih sehat → PD turun",
1370
- "About savings and your cash flow (currently **Rp " + str(int(cf/1e6)) + "M/month**):\n\n"
1371
- "3 practical SME savings tips:\n"
1372
- "1. Separate business & personal bank accounts\n"
1373
- "2. Auto-transfer 10-15% of monthly revenue to savings\n"
1374
- "3. Document all income streams — this also boosts digital score!\n\n"
1375
- "More savings → healthier cash flow → lower PD",
1376
- "Bachat aur naqad pravaah (**Rp " + str(int(cf/1e6)) + "M/maah**) ke baare mein:\n\n"
1377
- "3 vyaavhaarik tarike:\n"
1378
- "1. Vyapaar aur vyaktigat khaate alag rakhein\n"
1379
- "2. Maasik aay ka 10-15% bachaen\n"
1380
- "3. Sabhi aay document karein — digital score bhi badhega!\n\n"
1381
- "Zyada bachat → swasth naqad pravaah → PD kam"
1382
- )
1383
- adjustments['monthly_cash_flow'] = int(cf * 1.3)
1384
- elif any(w in low for w in ['riwayat kredit','credit history','kredit history','credit record','riwayat','history kredit']):
1385
- ch_val = raw_input.get('credit_history', '-')
1386
- response = _t(
1387
- "**Riwayat kredit** = catatan sejarah bayar utangmu — ibarat rapor keuangan.\n\n"
1388
- "Status kamu: **" + ch_val + "**\n\n"
1389
- "Dampak ke skor kredit:\n"
1390
- "- All paid / Existing paid → sinyal positif\n"
1391
- "- Delayed previously → bank waspada\n"
1392
- "- Critical → risiko naik signifikan\n\n"
1393
- "Cara bangun riwayat bagus: bayar cicilan tepat waktu, jangan ambil pinjaman melebihi kemampuan bayar.",
1394
- "**Credit history** = your track record of paying debts — your financial report card.\n\n"
1395
- "Your status: **" + ch_val + "**\n\n"
1396
- "Score impact:\n"
1397
- "- All paid / Existing paid → positive signal\n"
1398
- "- Delayed previously → bank is cautious\n"
1399
- "- Critical → significant risk increase\n\n"
1400
- "Build good history: pay on time, don't borrow beyond your repayment capacity.",
1401
- "**Credit itihaas** = karz bhugtaan ka record — vitteey report card.\n\n"
1402
- "Aapki sthiti: **" + ch_val + "**\n\n"
1403
- "Score prabhav:\n"
1404
- "- All paid / Existing paid → sakaraatmak sanket\n"
1405
- "- Delayed previously → bank saavdhan\n"
1406
- "- Critical → jokhim zyada\n\n"
1407
- "Samay par bhugtaan karein, kshamta se adhik rin na lein."
1408
- )
1409
- elif any(w in low for w in ['tips','bisnis','usaha','umkm','sme','digital','marketplace','online','ecommerce','strategi','business tips']):
1410
- response = _t(
1411
- "Tips bisnis UMKM buat kamu (PD " + str(round(pd_pct,1)) + "%):\n\n"
1412
- "1. **Digital hadir** — daftar Google Business Profile, aktif di Tokopedia/Shopee/TikTok Shop\n"
1413
- " (Digital score kamu " + str(dig) + "/100 — masih bisa naik!)\n"
1414
- "2. **Pisah keuangan** — rekening bisnis terpisah dari pribadi\n"
1415
- "3. **Dokumentasi rutin** — catat semua transaksi, ini bukti ke bank\n"
1416
- "4. **Legalitas** — NPWP & SIUP/NIB buka akses ke KUR & kredit formal\n\n"
1417
- "Mau aku simulasikan dampaknya ke skor di tab What-If?",
1418
- "SME business tips for you (PD " + str(round(pd_pct,1)) + "%):\n\n"
1419
- "1. **Go digital** — Google Business Profile, Tokopedia/Shopee/TikTok Shop\n"
1420
- " (Your digital score " + str(dig) + "/100 — room to improve!)\n"
1421
- "2. **Separate finances** — dedicated business bank account\n"
1422
- "3. **Document everything** — track all transactions as proof for banks\n"
1423
- "4. **Get legal** — NPWP & SIUP/NIB unlock KUR & formal credit\n\n"
1424
- "Want me to simulate the impact in the What-If tab?",
1425
- "SME vyapaar tips (PD " + str(round(pd_pct,1)) + "%):\n\n"
1426
- "1. **Digital upasthiti** — Google Business Profile, Tokopedia/Shopee\n"
1427
- " (Aapka digital score " + str(dig) + "/100 — sudhaar ki gunjaish!)\n"
1428
- "2. **Alag vitteey khaata** — vyapaar ka alag bank khaata\n"
1429
- "3. **Sab document karein** — sabhi laanden-denden ka record\n"
1430
- "4. **Kanooni rahein** — NPWP & SIUP/NIB KUR unlock karta hai\n\n"
1431
- "Kya main What-If tab mein prabhav simulate karun?"
1432
  )
1433
  if dig < 75:
1434
  adjustments['digital_presence_score'] = 75
@@ -1437,15 +1275,9 @@ CARA BERPIKIR — CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
1437
  has_key = any([st.session_state.get("openrouter_key"), st.session_state.get("groq_key")])
1438
  err_note = "\n\nLLM error: " + str(_last_error)[:80] if (has_key and _last_error) else ""
1439
  response = _t(
1440
- "Pertanyaan menarik! Untuk ini aku butuh koneksi AI.\n\n"
1441
- "Yang bisa aku kasih tahu: PD kamu **" + str(round(pd_pct,1)) + "%** dan " + top_issue + "\n\n"
1442
- "Coba tanya yang lebih spesifik: cara turunkan skor, faktor risiko, pinjaman ideal, tips tabungan, atau riwayat kredit." + err_note,
1443
- "Great question! For this I need an AI connection.\n\n"
1444
- "What I can share: your PD is **" + str(round(pd_pct,1)) + "%** and " + top_issue + "\n\n"
1445
- "Try asking specifically: how to lower score, risk factors, ideal loan, savings tips, or credit history." + err_note,
1446
- "Accha sawaal! Iske liye AI connection chahiye.\n\n"
1447
- "Aapka PD **" + str(round(pd_pct,1)) + "%** hai aur " + top_issue + "\n\n"
1448
- "Vishesh roop se puchein: score kaise kam karein, jokhim kaarak, ideal rin, bachat tips." + err_note
1449
  )
1450
 
1451
  if response:
@@ -1457,8 +1289,7 @@ CARA BERPIKIR — CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
1457
  return response or "...", adjustments, _last_error
1458
 
1459
  # ============================================================
1460
- # FLOATING DRAGGABLE GIF + SOUND (FIXED: window._sme state)
1461
- # FIX: Removed Python # comment inside JS string
1462
  # ============================================================
1463
  @st.cache_resource(show_spinner=False)
1464
  def _load_media_b64():
@@ -1481,22 +1312,37 @@ def _load_media_b64():
1481
  }
1482
 
1483
  def inject_media_manager():
 
 
 
 
 
 
 
 
 
 
1484
  media = _load_media_b64()
1485
- if not any(media.get(k, '') for k in ['crying_cat', 'dance1']):
1486
- return
1487
 
1488
  muted = st.session_state.get('gif_muted', False)
1489
  vol_frac = 0.0 if muted else 0.35
1490
  vf_str = str(vol_frac)
1491
 
1492
  def _gif(gid, b64):
1493
- if not b64: return ""
 
1494
  return (
1495
  '<div id="' + gid + '" style="'
1496
- 'display:none;position:fixed;z-index:99999;pointer-events:none;'
1497
- 'transition:left 1.1s cubic-bezier(.4,0,.2,1),top 1.1s cubic-bezier(.4,0,.2,1);'
1498
- 'filter:drop-shadow(0 4px 16px rgba(0,0,0,.6));left:50px;top:50px;">'
1499
- '<img src="' + b64 + '" style="width:110px;height:auto;border-radius:14px;display:block;">'
 
 
 
 
 
1500
  '</div>'
1501
  )
1502
 
@@ -1515,532 +1361,592 @@ def inject_media_manager():
1515
  dom_js = dom_str.replace('\\', '\\\\').replace('`', '\\`').replace('${', '\\${')
1516
  sa_safe = sa.replace('\\', '\\\\').replace("'", "\\'")
1517
  sw_safe = sw.replace('\\', '\\\\').replace("'", "\\'")
 
1518
 
1519
- # FIX: removed "# Apply current tab state immediately" Python comment
1520
- # that was being embedded as raw text inside the JS string.
1521
- # JS comments use // not #.
1522
  js = (
1523
  "(function(){"
1524
- "var d=document,w=window;"
1525
-
1526
- "if(!w._sme){"
1527
- "w._sme={"
1528
- "ct:-1,"
1529
- "att:new WeakSet(),"
1530
- "rt:null,"
1531
- "vol:" + vf_str + ","
1532
- "};"
1533
-
1534
- "if(!d.getElementById('sme-media-root')){"
1535
- "var root=d.createElement('div');root.id='sme-media-root';"
1536
- "root.innerHTML=`" + dom_js + "`;"
1537
- "var as=d.getElementById('sme-aud-shap');"
1538
- "var aw=d.getElementById('sme-aud-wi');"
1539
- "if(as)as.setAttribute('src','" + sa_safe + "');"
1540
- "if(aw)aw.setAttribute('src','" + sw_safe + "');"
1541
- "d.body.appendChild(root);"
1542
  "}"
1543
 
 
1544
  "var GS=['sme-gif-shap'];"
1545
  "var GW=['sme-gif-wi1','sme-gif-wi2','sme-gif-wi3','sme-gif-wi4','sme-gif-wi5'];"
1546
  "var GA=GS.concat(GW);"
1547
- "var ZN=[[0.03,0.65],[0.82,0.65],[0.40,0.72],[0.03,0.08],[0.82,0.08],"
1548
- "[0.03,0.38],[0.82,0.38],[0.22,0.68],[0.62,0.68],[0.22,0.08],"
1549
- "[0.62,0.08],[0.50,0.65],[0.15,0.50],[0.75,0.50]];"
 
 
1550
  "var LZ={};"
1551
 
1552
- "w._sme.occ=function(sk){"
1553
- "var o=[];GA.forEach(function(id){"
 
 
1554
  "if(id===sk)return;"
1555
- "var el=d.getElementById(id);"
1556
  "if(!el||el.style.display==='none')return;"
1557
- "o.push({x:el.offsetLeft,y:el.offsetTop});});return o;};"
 
1558
 
1559
- "w._sme.rz=function(gid,oc){"
1560
- "var ww=w.innerWidth,wh=w.innerHeight,sz=120;"
 
1561
  "var sh=ZN.slice().sort(function(){return Math.random()-0.5;});"
1562
  "var pv=LZ[gid]!=null?LZ[gid]:-1;"
1563
  "var ca=sh.filter(function(z,i){return i!==pv;});"
1564
  "if(!ca.length)ca=sh;"
1565
  "for(var i=0;i<ca.length;i++){"
1566
- "var z=ca[i];"
1567
- "var px=Math.min(z[0]*ww,ww-sz-10),py=Math.min(z[1]*wh,wh-sz-10);"
1568
  "var ok=true;"
1569
  "for(var j=0;j<oc.length;j++){"
1570
  "var dx=px-oc[j].x,dy=py-oc[j].y;"
1571
- "if(Math.sqrt(dx*dx+dy*dy)<150){ok=false;break;}}"
1572
  "if(ok){LZ[gid]=ZN.indexOf(z);return{x:px,y:py};}}"
1573
  "LZ[gid]=ZN.indexOf(ca[0]);"
1574
- "return{x:Math.min(ca[0][0]*ww,ww-sz-10),y:Math.min(ca[0][1]*wh,wh-sz-10)};};"
 
1575
 
1576
- "w._sme.mv=function(gid){"
1577
- "var el=d.getElementById(gid);"
 
 
1578
  "if(!el||el.style.display==='none')return;"
1579
- "var p=w._sme.rz(gid,w._sme.occ(gid));"
1580
  "el.style.left=p.x+'px';el.style.top=p.y+'px';"
1581
  "el.style.right='auto';el.style.bottom='auto';};"
1582
 
1583
- "w._sme.pa=function(id){"
1584
- "var a=d.getElementById(id);"
1585
- "if(a){a.volume=w._sme.vol;a.play().catch(function(){});}};"
1586
-
1587
- "w._sme.stop=function(){"
 
 
 
 
 
 
1588
  "GA.forEach(function(id){"
1589
- "var el=d.getElementById(id);if(el)el.style.display='none';});"
1590
  "['sme-aud-shap','sme-aud-wi'].forEach(function(id){"
1591
- "var a=d.getElementById(id);"
1592
- "if(a){a.pause();a.currentTime=0;}});};"
1593
-
1594
- "w._sme.showS=function(){"
1595
- "w._sme.stop();"
1596
- "var el=d.getElementById('sme-gif-shap');if(!el)return;"
1597
- "var p=w._sme.rz('sme-gif-shap',[]);"
 
 
1598
  "el.style.left=p.x+'px';el.style.top=p.y+'px';"
1599
  "el.style.right='auto';el.style.bottom='auto';"
1600
  "el.style.display='block';"
1601
- "w._sme.pa('sme-aud-shap');};"
1602
 
1603
- "w._sme.showW=function(){"
1604
- "w._sme.stop();"
1605
- "var ww=w.innerWidth,wh=w.innerHeight,sz=110;"
 
1606
  "var sp=[0.03,0.22,0.41,0.60,0.79];"
1607
  "GW.forEach(function(gid,i){"
1608
- "var el=d.getElementById(gid);if(!el)return;"
1609
  "el.style.left=Math.min(sp[i]*ww,ww-sz-10)+'px';"
1610
  "el.style.top=Math.min(0.68*wh,wh-sz-10)+'px';"
1611
  "el.style.right='auto';el.style.bottom='auto';"
1612
  "el.style.display='block';"
1613
  "LZ[gid]=-1;});"
1614
- "w._sme.pa('sme-aud-wi');};"
1615
-
1616
- "w._sme.tidx=function(){"
1617
- "var all=d.querySelectorAll('[data-baseweb=\"tab\"]');"
1618
- "if(!all||!all.length)all=d.querySelectorAll('[role=\"tab\"]');"
1619
- "if(!all||!all.length)return-1;"
 
 
 
 
 
 
 
 
 
1620
  "for(var i=0;i<all.length;i++){"
1621
  "if(all[i].getAttribute('aria-selected')==='true')return i;}"
1622
  "return 0;};"
1623
 
1624
- "w._sme.apT=function(idx){"
1625
- "if(idx===w._sme.ct)return;"
1626
- "w._sme.ct=idx;"
1627
- "if(idx===0)w._sme.showS();"
1628
- "else if(idx===3)w._sme.showW();"
1629
- "else w._sme.stop();};"
1630
-
1631
- "if(w._sme.rt)clearInterval(w._sme.rt);"
1632
- "w._sme.rt=setInterval(function(){GA.forEach(w._sme.mv);},5000);"
1633
-
1634
- "}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1635
 
1636
- "w._sme.vol=" + vf_str + ";"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1637
  "['sme-aud-shap','sme-aud-wi'].forEach(function(id){"
1638
- "var a=d.getElementById(id);if(a)a.volume=w._sme.vol;});"
1639
 
 
 
 
 
 
 
 
1640
  "function addC(){"
1641
- "var tabs=d.querySelectorAll('[data-baseweb=\"tab\"]');"
1642
- "if(!tabs||tabs.length<2)tabs=d.querySelectorAll('[role=\"tab\"]');"
1643
  "if(!tabs||tabs.length<2)return false;"
1644
  "tabs.forEach(function(tab,i){"
1645
- "if(w._sme.att.has(tab))return;"
1646
- "w._sme.att.add(tab);"
1647
  "tab.addEventListener('click',function(){"
1648
- "setTimeout(function(){w._sme.apT(w._sme.tidx());},120);});});"
1649
  "return true;}"
1650
 
1651
- "var tr=0,pl=setInterval(function(){"
1652
  "tr++;"
1653
  "if(addC()){"
1654
- "var ci=w._sme.tidx();" # FIX: was "# Apply current tab state immediately" here
1655
- "if(ci!==w._sme.ct){w._sme.ct=-1;w._sme.apT(ci);}"
1656
- "clearInterval(pl);"
1657
- "setInterval(function(){addC();},2000);}"
1658
- "if(tr>80)clearInterval(pl);},150);"
 
1659
 
1660
  "})();"
1661
  )
1662
 
1663
- st.markdown('<script>' + js + '</script>', unsafe_allow_html=True)
 
 
1664
 
1665
  # ============================================================
1666
  # HEADER
1667
  # ============================================================
1668
- roc_b = ("<span class='badge b-blue'>ROC-AUC: " + str(meta['roc_auc']) + "</span>") if meta.get('roc_auc') else ""
1669
- ks_b = ("<span class='badge b-green'>KS: " + str(meta['ks_stat']) + "</span>") if meta.get('ks_stat') else ""
1670
  st.markdown(
1671
- "<div class='header-wrap'>"
1672
- "<h1>" + T('header_title',lang) + "</h1>"
1673
- "<p>" + T('header_sub',lang) + "</p>"
1674
- "<div style='margin-top:0.8rem'>"
1675
- "<span class='badge b-blue'>XGBoost + LightGBM + RF</span>"
1676
- "<span class='badge b-green'>SHAP XAI</span>"
1677
- "<span class='badge b-yellow'>LLM Narrative</span>"
1678
- "<span class='badge b-blue'>Maya + Tool Calling</span>"
1679
- "<span class='badge b-green'>CoT + Few-shot</span>"
1680
- "<span class='badge b-yellow'>Memory</span>"
1681
- + roc_b + ks_b +
1682
- "</div></div>",
1683
  unsafe_allow_html=True
1684
  )
1685
 
1686
  # ============================================================
1687
- # INPUT FORM
1688
  # ============================================================
1689
- st.markdown('<div class="sec-title">' + T("form_title",lang) + '</div>', unsafe_allow_html=True)
1690
  with st.form("credit_form"):
1691
- c1, c2, c3 = st.columns(3)
1692
- with c1:
 
 
1693
  st.markdown(T('form_loan', lang))
1694
- loan_rp = st.number_input(T('f_loan_amt',lang), 5_000_000, 500_000_000, 50_000_000, 5_000_000, format="%d")
1695
- lgd_val = st.slider(T('f_lgd',lang), 0.20, 0.80, 0.40, 0.05)
1696
- duration = st.slider(T('f_duration',lang), 4, 72, 24)
1697
- credit_amt = st.number_input(T('f_credit_amt',lang), 250, 20000, 2500, 250)
1698
- purpose = st.selectbox(T('f_purpose',lang), PURPOSE_OPTS, index=9,
1699
- format_func=lambda x: PURPOSE_LABELS[x][lang])
1700
- with c2:
 
 
 
 
 
 
 
1701
  st.markdown(T('form_financial', lang))
1702
- age = st.slider(T('f_age',lang), 18, 75, 35)
1703
- checking = st.selectbox(T('f_checking',lang), CHECKING_OPTS,
1704
- format_func=lambda x: CHECKING_LABELS[x][lang])
1705
- savings = st.selectbox(T('f_savings',lang), SAVINGS_OPTS, index=4,
1706
- format_func=lambda x: SAVINGS_LABELS[x][lang])
1707
- credit_hist = st.selectbox(T('f_credit_hist',lang), CREDIT_H_OPTS, index=2,
1708
- format_func=lambda x: CREDIT_H_LABELS[x][lang])
1709
- employment = st.selectbox(T('f_employment',lang), EMPLOY_OPTS, index=2,
1710
- format_func=lambda x: EMPLOY_LABELS[x][lang])
1711
- housing = st.selectbox(T('f_housing',lang), HOUSING_OPTS, index=2,
1712
- format_func=lambda x: HOUSING_LABELS[x][lang])
1713
- installment_c = st.slider(T('f_installment',lang), 1, 4, 2)
1714
- with c3:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1715
  st.markdown(T('form_sme', lang))
1716
  digital_score = st.slider(T('f_digital',lang), 1, 100, 50)
1717
- has_social = st.toggle(T('f_social',lang), True)
1718
- ecomm_vol = st.number_input(T('f_ecomm',lang), 0, 100_000_000, 5_000_000, 1_000_000)
1719
- has_npwp = st.toggle(T('f_npwp',lang), True)
1720
- has_siup = st.toggle(T('f_siup',lang), True)
1721
- biz_age = st.slider(T('f_biz_age',lang), 1, 20, 5)
1722
- cash_flow = st.number_input(T('f_cashflow',lang), 0, 100_000_000, 15_000_000, 1_000_000)
1723
- num_emp = st.slider(T('f_employees',lang), 1, 50, 5)
1724
- submitted = st.form_submit_button(T('f_submit',lang), width='stretch', type="primary")
1725
 
1726
- # ============================================================
1727
- # EMPTY STATE
1728
- # ============================================================
1729
- if not submitted and st.session_state.result is None:
1730
- st.markdown("<br>", unsafe_allow_html=True)
1731
- for col, (icon, lbl_k, val) in zip(st.columns(4), [
1732
- ('🤖','empty_ensemble','XGB+LGBM+RF'),
1733
- ('🔍','empty_xai','SHAP'),
1734
- ('💬','empty_narrative','Multi-LLM'),
1735
- ('🤖','empty_chat','Maya AI'),
1736
- ]):
1737
- with col:
1738
- st.markdown(
1739
- '<div class="kpi-box"><div style="font-size:1.8rem">' + icon + '</div>'
1740
- '<div class="kpi-lbl">' + T(lbl_k,lang) + '</div>'
1741
- '<div style="font-size:.85rem;font-weight:700;color:#1a2c6b">' + val + '</div></div>',
1742
- unsafe_allow_html=True
1743
- )
1744
- if meta:
1745
- st.markdown("<br>", unsafe_allow_html=True)
1746
- for col, (lbl, key) in zip(st.columns(4), [
1747
- ('ROC-AUC','roc_auc'),('KS Stat','ks_stat'),('CV Mean','cv_mean'),('Features','n_features')
1748
- ]):
1749
- with col:
1750
- st.markdown(
1751
- '<div class="kpi-box"><div class="kpi-lbl">' + lbl + '</div>'
1752
- '<div class="kpi-val">' + str(meta.get(key,"—")) + '</div></div>',
1753
- unsafe_allow_html=True
1754
- )
1755
- st.stop()
1756
 
1757
  # ============================================================
1758
- # PREDICTION
1759
  # ============================================================
1760
  if submitted:
1761
  raw = {
1762
- 'checking_status': checking, 'duration': duration, 'credit_history': credit_hist,
1763
- 'purpose': purpose, 'credit_amount': credit_amt, 'savings_status': savings,
1764
- 'employment': employment, 'installment_commitment': installment_c,
1765
- 'personal_status': 'male single', 'other_parties': 'none', 'residence_since': 3,
1766
- 'property_magnitude': 'real estate', 'age': age, 'other_payment_plans': 'none',
1767
- 'housing': housing, 'existing_credits': 1, 'job': 'skilled', 'num_dependents': 1,
1768
- 'own_telephone': 'yes', 'foreign_worker': 'yes',
1769
- 'digital_presence_score': digital_score, 'has_social_media': int(has_social),
1770
- 'ecommerce_monthly_volume': ecomm_vol, 'has_npwp': int(has_npwp), 'has_siup': int(has_siup),
1771
- 'business_age_years': float(biz_age), 'monthly_cash_flow': float(cash_flow),
1772
- 'num_employees': num_emp, 'loan_rp': loan_rp,
1773
- }
1774
- _step_labels = {
1775
- 'id': ['🔢 Memproses data input...', '🤖 Menjalankan model ensemble...', '🔍 Menghitung SHAP values...', '💬 Menghasilkan narasi AI...', '✅ Selesai!'],
1776
- 'en': ['🔢 Processing input data...', '🤖 Running ensemble model...', '🔍 Computing SHAP values...', '💬 Generating AI narrative...', '✅ Done!'],
1777
- 'hi': ['🔢 Input data process ho raha hai...', '🤖 Ensemble model chal raha hai...', '🔍 SHAP values compute ho rahe hain...', '💬 AI vivarana ban raha hai...', '✅ Taiyaar!'],
 
 
 
1778
  }
1779
- _steps = _step_labels.get(lang, _step_labels['id'])
1780
- _prog_ph = st.empty()
1781
- _status_ph = st.empty()
1782
-
1783
- def _show_step(idx):
1784
- pct = int((idx / (len(_steps) - 1)) * 100)
1785
- _prog_ph.progress(pct, text=_steps[idx])
1786
- _status_ph.caption("⏱ " + _steps[idx])
1787
 
1788
- try:
1789
- _show_step(0)
1790
- X_in = preprocess(raw, scaler, feature_names)
 
 
 
1791
 
1792
- _show_step(1)
1793
- pd_score = float(ensemble.predict_proba(X_in)[0][1])
1794
- result = risk_result(pd_score, loan_rp, lgd_val)
1795
 
1796
- _show_step(2)
1797
- try:
1798
- sv = explainer(X_in)
1799
- shap_vals = sv[0].values
1800
- if not isinstance(shap_vals, np.ndarray) or len(shap_vals) == 0:
1801
- raise ValueError("SHAP values empty or invalid")
1802
- shap_png = make_shap_png(shap_vals, feature_names)
1803
- except Exception as shap_err:
1804
- shap_vals = np.zeros(len(feature_names))
1805
- shap_png = None
1806
- st.warning("⚠️ SHAP explainer error: " + str(shap_err)[:120] + " — SHAP tab akan menampilkan nilai 0.")
1807
-
1808
- _show_step(3)
1809
- narrative, llm_src = get_narrative(shap_vals, feature_names, result, lang, raw)
1810
-
1811
- _show_step(4)
1812
  st.session_state.result = result
1813
- st.session_state.shap_vals = shap_vals
1814
  st.session_state.raw_input = raw
1815
  st.session_state.narrative = narrative
1816
  st.session_state.llm_src = llm_src
1817
  st.session_state.narrative_lang = lang
1818
- st.session_state.shap_png = shap_png
1819
- st.session_state.chat_history = []
1820
- st.session_state.chat_summary = ''
1821
- for k in ['wi_dig','wi_biz','wi_emp','wi_cash','wi_dur','wi_loan']:
1822
- st.session_state[k] = None
1823
- _save_chat_memory([], '')
1824
-
1825
- _prog_ph.empty()
1826
- _status_ph.empty()
1827
- except Exception as e:
1828
- _prog_ph.empty()
1829
- _status_ph.empty()
1830
- st.error("Error: " + str(e))
1831
- st.stop()
1832
-
1833
- result = st.session_state.result
1834
- shap_vals = st.session_state.shap_vals
1835
- raw_input = st.session_state.raw_input
1836
- narrative = st.session_state.narrative
1837
- llm_src = st.session_state.llm_src
1838
- if result is None:
1839
- st.stop()
1840
-
1841
- if result is not None and raw_input is not None and st.session_state.narrative_lang != lang:
1842
- with st.spinner(T('spinner', lang)):
1843
- narrative, llm_src = get_narrative(shap_vals, feature_names, result, lang, raw_input)
1844
- st.session_state.narrative = narrative
1845
- st.session_state.llm_src = llm_src
1846
- st.session_state.narrative_lang = lang
1847
- st.session_state.chat_history = []
1848
- st.session_state.chat_summary = ''
1849
 
1850
  # ============================================================
1851
- # RESULTS BANNER + KPIs
1852
  # ============================================================
1853
- st.markdown("---")
1854
- st.markdown(
1855
- "<div class='risk-banner " + result['css'] + "'>"
1856
- + result['cat'][lang] +
1857
- "<div style='font-size:.95rem;font-weight:500;opacity:.85;margin-top:.3rem'>"
1858
- + T('kpi_pd',lang) + ": " + str(round(result['pd']*100,1)) + "%"
1859
- "</div></div>",
1860
- unsafe_allow_html=True
1861
- )
1862
 
1863
- for col, (lbl_k, val, sub_k, color) in zip(st.columns(4), [
1864
- ('kpi_pd', str(round(result['pd']*100,1)) + "%", 'kpi_pd_sub', result['color']),
1865
- ('kpi_el', "Rp " + str(round(result['el']/1e6,2)) + "jt", 'kpi_el_sub', "#e74c3c"),
1866
- ('kpi_lgd', str(int(result['lgd']*100)) + "%", 'kpi_lgd_sub', "#1a2c6b"),
1867
- ('kpi_ead', "Rp " + str(int(result['ead']/1e6)) + "jt", 'kpi_ead_sub', "#1a2c6b"),
1868
- ]):
1869
- with col:
 
 
 
 
1870
  st.markdown(
1871
- '<div class="kpi-box"><div class="kpi-lbl">' + T(lbl_k,lang) + '</div>'
1872
- '<div class="kpi-val" style="color:' + color + ';font-size:1.4rem">' + val + '</div>'
1873
- '<div class="kpi-sub">' + T(sub_k,lang) + '</div></div>',
1874
- unsafe_allow_html=True
 
1875
  )
1876
- st.markdown("<br>", unsafe_allow_html=True)
1877
-
1878
- inject_media_manager()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1879
 
1880
- # Tab loading progress
 
 
1881
  _tab_load_steps = {
1882
- 'id': ['🔍 Menyiapkan tab SHAP...','💬 Menyiapkan Narasi AI...','🤖 Menyiapkan AI Chat...','🎮 Menyiapkan What-If...','📐 Menyiapkan Formula...','✅ Semua tab siap!'],
1883
- 'en': ['🔍 Preparing SHAP tab...','💬 Preparing AI Narrative...','🤖 Preparing AI Chat...','🎮 Preparing What-If...','📐 Preparing Formula...','✅ All tabs ready!'],
1884
- 'hi': ['🔍 SHAP tab taiyaar ho raha hai...','💬 AI Vivarana taiyaar ho raha hai...','🤖 AI Chat taiyaar ho raha hai...','🎮 What-If taiyaar ho raha hai...','📐 Sutra taiyaar ho raha hai...','✅ Sabhi tab taiyaar!'],
 
 
 
1885
  }
1886
- _tls = _tab_load_steps.get(lang, _tab_load_steps['id'])
1887
- _tab_prog_ph = st.empty()
1888
- _tab_label_ph = st.empty()
1889
 
1890
  def _tab_step(n):
1891
- if result is not None:
1892
  pct = int(n / 5 * 100)
1893
  _tab_prog_ph.progress(pct, text=_tls[n])
 
 
1894
 
1895
  _tab_step(0)
1896
 
 
1897
  # TABS
 
1898
  t1, t2, t3, t4, t5 = st.tabs([
1899
  T('tab_shap',lang), T('tab_narrative',lang),
1900
  T('tab_chat',lang), T('tab_whatif',lang), T('tab_formula',lang)
1901
  ])
1902
 
1903
- # TAB 1: SHAP
1904
  with t1:
1905
  _tab_step(0)
1906
  st.markdown('<div class="sec-title">' + T("shap_title",lang) + '</div>', unsafe_allow_html=True)
1907
- _shap_all_zero = (shap_vals is not None and np.all(shap_vals == 0))
1908
- if _shap_all_zero:
1909
- st.warning(
1910
- "⚠️ **SHAP tidak tersedia** model XGBoost (`xgb_model.pkl`) mungkin belum di-upload atau "
1911
- "tidak kompatibel dengan SHAP TreeExplainer. "
1912
- "Pastikan `model/xgb_model.pkl` ada di HF Space dan di-train ulang jika perlu.\n\n"
1913
- "Skor PD dan narasi tetap valid (menggunakan ensemble model)."
1914
- )
1915
  else:
1916
- if st.session_state.get("shap_png"):
1917
- _spng = st.session_state.shap_png
1918
- if isinstance(_spng, str) and _spng.startswith('data:'):
1919
- st.markdown('<img src="' + _spng + '" style="width:100%;border-radius:8px;">', unsafe_allow_html=True)
1920
- elif _spng:
1921
- st.image(_spng, width='stretch')
1922
- st.caption(T('shap_caption', lang))
1923
- top_idx = np.argsort(np.abs(shap_vals))[-10:][::-1]
1924
- st.dataframe(pd.DataFrame({
1925
- T('shap_col_feature',lang): [feature_names[i] for i in top_idx],
1926
- 'SHAP': [round(shap_vals[i],4) for i in top_idx],
1927
- T('shap_col_impact',lang): [
1928
- T('shap_risk_up',lang) if shap_vals[i] > 0 else T('shap_risk_down',lang)
1929
- for i in top_idx
1930
- ],
1931
- }), width='stretch', hide_index=True)
1932
-
1933
- # TAB 2: NARRATIVE
 
 
 
 
 
 
 
1934
  with t2:
1935
  _tab_step(1)
1936
  flag = {'id':'🇮🇩','en':'🇬🇧','hi':'🇮🇳'}[lang]
1937
  st.markdown('<div class="sec-title">' + T("narr_title",lang) + ' ' + flag + '</div>', unsafe_allow_html=True)
1938
- st.caption(T('narr_source',lang) + ": " + (llm_src if llm_src else "—"))
1939
- if narrative and narrative.strip():
1940
- fmt = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', narrative.replace('\n','<br>'))
1941
- st.markdown('<div class="narrative-box">' + fmt + '</div>', unsafe_allow_html=True)
 
1942
  else:
1943
- narrative_fb, src_fb = get_narrative(shap_vals, feature_names, result, lang, raw_input)
1944
- st.session_state.narrative = narrative_fb
1945
- st.session_state.llm_src = src_fb
1946
- fmt = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', narrative_fb.replace('\n','<br>'))
1947
- st.markdown('<div class="narrative-box">' + fmt + '</div>', unsafe_allow_html=True)
 
 
 
 
 
1948
  _or_key = st.session_state.get("openrouter_key","")
1949
  _grq_key = st.session_state.get("groq_key","")
1950
- with st.expander("LLM Debug Info"):
1951
  st.code(
1952
  "OR key : " + ('OK ' + _or_key[:12] + '...' if _or_key else 'missing') + "\n"
1953
  "Groq key : " + ('OK ' + _grq_key[:12] + '...' if _grq_key else 'missing') + "\n"
1954
- "Source : " + str(src_fb) + "\n"
1955
  "OR model : " + _OR_FREE_MODELS[0] + "\n"
1956
  "Groq mdl : " + _GROQ_FREE_MODELS[0]
1957
  )
1958
 
1959
- # TAB 3: AI CHAT
1960
  with t3:
1961
  _tab_step(2)
1962
  st.markdown('<div class="sec-title">' + T("chat_title",lang) + '</div>', unsafe_allow_html=True)
1963
 
1964
- if st.session_state.get('chat_summary'):
1965
- n_turns = len(st.session_state.chat_history)
1966
- lbl = {
1967
- 'id': 'Memory aktif - ' + str(n_turns) + ' pesan tersimpan',
1968
- 'en': 'Memory active - ' + str(n_turns) + ' messages saved',
1969
- 'hi': 'Memory active - ' + str(n_turns) + ' sandesh save hue',
1970
- }
1971
- st.markdown('<div class="memory-badge">' + lbl.get(lang, lbl["en"]) + '</div>', unsafe_allow_html=True)
1972
-
1973
- chips = [T('chat_chip1',lang), T('chat_chip2',lang), T('chat_chip3',lang), T('chat_chip4',lang)]
1974
- chip_cols = st.columns(len(chips))
1975
- chip_clicked = None
1976
- for i, (col, chip) in enumerate(zip(chip_cols, chips)):
1977
- with col:
1978
- if st.button(chip, key="chip_" + str(i), width='stretch'):
1979
- chip_clicked = chip
1980
- st.markdown("---")
1981
-
1982
- if st.session_state.chat_history:
1983
- html_chat = ""
1984
- for msg in st.session_state.chat_history:
1985
- content = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>',
1986
- msg['content'].replace('\n','<br>'))
1987
- cls = "chat-bubble-user" if msg['role'] == 'user' else "chat-bubble-ai"
1988
- icon = "👤" if msg['role'] == 'user' else "🤖"
1989
- html_chat += '<div class="' + cls + '">' + icon + ' ' + content + '</div>'
1990
- st.markdown(html_chat, unsafe_allow_html=True)
1991
  else:
1992
- st.info("👋 " + T('chat_welcome', lang))
1993
-
1994
- user_input = st.chat_input(T('chat_input', lang))
1995
- to_process = chip_clicked or user_input
1996
- if to_process:
1997
- st.session_state.chat_history.append({'role':'user','content':to_process})
1998
- _chat_status_ph = st.empty()
1999
- _spinner_label = {
2000
- 'id': '🤔 Maya sedang berpikir...',
2001
- 'en': '🤔 Maya is thinking...',
2002
- 'hi': '🤔 Maya soch rahi hai...',
2003
- }.get(lang, '🤔 Maya sedang berpikir...')
2004
- with st.spinner(_spinner_label):
2005
- _chat_status_ph.info(
2006
- {'id':'⏳ Menghubungi LLM — bisa 5–15 detik tergantung model yang dipilih...',
2007
- 'en':'⏳ Connecting to LLM — may take 5–15 seconds depending on the model...',
2008
- 'hi':'⏳ LLM se connect ho raha hai — model ke hisaab se 5–15 second lag sakte hain...'}[lang],
2009
- icon="⏳"
2010
- )
2011
- ai_resp, adjustments, _debug_err = get_chat_response(
2012
- to_process, st.session_state.chat_history,
2013
- result, raw_input, shap_vals, feature_names, lang,
2014
- rag_index=rag_index
2015
- )
2016
- _chat_status_ph.empty()
2017
- if _debug_err and any([st.session_state.get("openrouter_key"),
2018
- st.session_state.get("groq_key")]):
2019
- st.warning("Semua LLM gagal, menggunakan Smart Fallback. Error: " + str(_debug_err)[:100])
2020
- st.session_state.chat_history.append({'role':'assistant','content':ai_resp})
2021
- _save_chat_memory(st.session_state.chat_history, st.session_state.get('chat_summary',''))
2022
- adj_map = {
2023
- 'digital_presence_score': 'wi_dig', 'business_age_years': 'wi_biz',
2024
- 'num_employees': 'wi_emp', 'monthly_cash_flow': 'wi_cash',
2025
- 'duration': 'wi_dur', 'loan_rp': 'wi_loan',
2026
- }
2027
- if adjustments:
2028
- for field, val in adjustments.items():
2029
- if field in adj_map:
2030
- st.session_state[adj_map[field]] = val
2031
- st.success("💡 " + T('chat_updated', lang))
2032
- st.rerun()
2033
 
2034
- if st.session_state.chat_history:
2035
- if st.button(T('chat_clear', lang)):
2036
- st.session_state.chat_history = []
2037
- st.session_state.chat_summary = ''
2038
- _save_chat_memory([], '')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2039
  st.rerun()
2040
 
2041
- # TAB 4: WHAT-IF
 
 
 
 
 
 
 
2042
  with t4:
2043
  _tab_step(3)
 
2044
  @st.fragment
2045
  def render_whatif():
2046
  _lang = st.session_state.get("lang_sel","id")
@@ -2145,25 +2051,15 @@ with t4:
2145
  with st.expander(T('wi_tips_title', _lang)):
2146
  tips = []
2147
  if wi_dig < 70:
2148
- tips.append(_t("📱 Naikkan Digital Score ke 70+ — marketplace & Google Business",
2149
- "📱 Raise Digital Score to 70+ — marketplace & Google Business",
2150
- "📱 Digital Score 70+ karein — marketplace & Google Business"))
2151
  if wi_cash < 20:
2152
- tips.append(_t("💵 Target cash flow Rp 20jt+/bulan",
2153
- "💵 Target Rp 20M+/month cash flow",
2154
- "💵 Naqad pravaah Rp 20M+/maah target"))
2155
  if wi_biz < 3:
2156
- tips.append(_t("🏢 Bisnis < 3 thn lebih berisiko — bangun track record",
2157
- "🏢 Business < 3 yr is riskier — build track record",
2158
- "🏢 3 saal se kam vyapaar — track record banaen"))
2159
  if wi_emp < 5:
2160
- tips.append(_t("👥 Tambah karyawan = skala bisnis lebih sehat",
2161
- "👥 More employees signals healthy scale",
2162
- "👥 Zyada karmachaaree = swasth paimaana"))
2163
  if not tips:
2164
- tips.append(_t("🌟 Profil sudah optimal!",
2165
- "🌟 Profile already well-optimized!",
2166
- "🌟 Parichay pehle se anukoolit!"))
2167
  for tip in tips:
2168
  st.markdown(tip)
2169
 
@@ -2172,60 +2068,74 @@ with t4:
2172
 
2173
  render_whatif()
2174
 
2175
- # TAB 5: FORMULA
2176
  with t5:
2177
  _tab_step(4)
2178
  st.markdown('<div class="sec-title">' + T("form_formula_title",lang) + '</div>', unsafe_allow_html=True)
2179
- st.latex(r"EL = PD \times LGD \times EAD")
2180
- st.latex(
2181
- "EL = " + str(round(result['pd'],4)) + r" \times " + str(round(result['lgd'],2))
2182
- + r" \times Rp\," + "{:,}".format(int(result['ead']))
2183
- + r" = Rp\," + "{:,}".format(int(result['el']))
2184
- )
2185
- cf1, cf2 = st.columns(2)
2186
- with cf1:
2187
- st.markdown(T('formula_def', lang))
2188
- with cf2:
2189
- cat_lbl = (
2190
- T('formula_low',lang) if result['pd'] < .2 else
2191
- T('formula_medium',lang) if result['pd'] < .5 else
2192
- T('formula_high',lang)
2193
- )
2194
- st.markdown(
2195
- "| " + T('formula_komponen',lang) + " | " + T('formula_nilai',lang) + " |\n"
2196
- "|--|--|\n"
2197
- "| PD | " + str(round(result['pd']*100,1)) + "% (" + cat_lbl + ") |\n"
2198
- "| LGD | " + str(int(result['lgd']*100)) + "% |\n"
2199
- "| EAD | Rp " + str(int(result['ead']/1e6)) + "jt |\n"
2200
- "| **EL** | **Rp " + str(round(result['el']/1e6,2)) + "jt** |"
2201
  )
2202
- _log_api("Formula", "Local", True, 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2203
 
2204
- # Clear progress bar after all tabs rendered
 
 
 
 
 
2205
  _tab_step(5)
2206
  _tab_prog_ph.empty()
2207
- _tab_label_ph.empty()
 
 
2208
 
2209
  # ============================================================
2210
  # DOWNLOAD REPORT
2211
  # ============================================================
2212
- st.markdown("---")
2213
- report_txt = (
2214
- "SME CREDIT RISK REPORT — 1na37 AI · Batch 10\n" + "="*50 + "\n"
2215
- + T('kpi_pd',lang) + ": " + str(round(result['pd']*100,1)) + "% " + result['cat'][lang] + "\n"
2216
- + T('kpi_el',lang) + ": Rp " + "{:,}".format(int(result['el'])) + "\n"
2217
- "LGD: " + str(int(result['lgd']*100)) + "% (Basel II)\n"
2218
- "EAD: Rp " + "{:,}".format(int(result['ead'])) + "\n"
2219
- + "="*50 + "\n"
2220
- + T('narr_title',lang) + "\n" + narrative + "\n"
2221
- + "="*50 + "\n"
2222
- "TOP SHAP\n" + shap_summary(shap_vals, feature_names, 5) + "\n"
2223
- + "="*50 + "\n"
2224
- "DISCLAIMER: Educational use only. Not financial advice."
2225
- )
2226
- st.download_button(
2227
- T('download_btn', lang),
2228
- data=report_txt,
2229
- file_name=T('download_file', lang),
2230
- mime="text/plain"
2231
- )
 
 
3
  # Final Project | AI Engineering Bootcamp Batch 10
4
  # Author: 1na37
5
  # ============================================================
6
+ # FIX LIST:
7
+ # 1. inject_media_manager() now CALLED after tabs (was defined but never called)
8
+ # 2. result/shap_vals/raw_input pulled from session_state before tabs
9
+ # 3. Guards added: tabs show "submit form first" if result is None
10
+ # 4. SHAP tab guard for shap_vals None
11
+ # 5. Download button guarded (only shown after result exists)
12
+ # 6. Mute sync: inject_media_manager called every render after result
13
  # ============================================================
14
 
15
  import streamlit as st
 
18
  import matplotlib
19
  matplotlib.use('Agg')
20
  import matplotlib.pyplot as plt
21
+ import streamlit.components.v1 as components
22
  import base64
23
  import time as _time
24
  import shap, joblib, os, json, warnings, re, requests, io, hashlib
 
32
  )
33
 
34
  # ============================================================
35
+ # TRANSLATION SYSTEM
36
  # ============================================================
37
  TRANSLATIONS = {
38
  'sidebar_api': {'id':'🔑 API Keys (opsional)', 'en':'🔑 API Keys (optional)', 'hi':'🔑 API Keys (vaikalpik)'},
 
131
  'wi_pd_from': {'id':'dari', 'en':'from', 'hi':'se'},
132
  'wi_no_change': {'id':'Perubahan minimal pada skor risiko','en':'Minimal change in risk score','hi':'Jokhim score mein nyoonatam badlaav'},
133
  'wi_tips_title': {'id':'💡 Tips Optimasi Skor', 'en':'💡 Score Optimization Tips', 'hi':'💡 Score Sudhaar Tips'},
134
+ 'wi_form_first': {'id':'⬆️ Submit form terlebih dahulu untuk melihat simulasi.',
135
+ 'en':'⬆️ Submit the form first to see the simulation.',
136
+ 'hi':'⬆️ Pehle form submit karein simulation dekhne ke liye.'},
137
  'wi_approved': {'id':'LAYAK', 'en':'APPROVED', 'hi':'SWIKAARY'},
138
  'wi_review': {'id':'REVIEW', 'en':'REVIEW', 'hi':'SAMEEKSHA'},
139
  'wi_highrisk': {'id':'RISIKO TINGGI', 'en':'HIGH RISK', 'hi':'UCHCH JOKHIM'},
 
157
  'risk_approved': {'id':'🟢 LAYAK', 'en':'🟢 APPROVED', 'hi':'🟢 YOGYA'},
158
  'risk_review': {'id':'🟡 PERLU REVIEW', 'en':'🟡 REVIEW', 'hi':'🟡 SAMEEKSHA'},
159
  'risk_high': {'id':'🔴 BERISIKO TINGGI', 'en':'🔴 HIGH RISK', 'hi':'🔴 UCHCH JOKHIM'},
 
 
 
 
160
  }
161
 
162
  def T(key: str, lang: str) -> str:
 
164
  return entry.get(lang, entry.get('en', key))
165
 
166
  # ============================================================
167
+ # SELECTBOX LABELS
168
  # ============================================================
169
  CHECKING_OPTS = ['<0', '0<=X<200', '>=200', 'no checking']
170
  CHECKING_LABELS = {
 
220
  LANG_LABELS = {'id':'🇮🇩 Bahasa Indonesia','en':'🇬🇧 English','hi':'🇮🇳 Hindi (Roman)'}
221
 
222
  # ============================================================
223
+ # CSS
224
  # ============================================================
225
  st.markdown("""
226
  <style>
 
295
  st.session_state.api_log = st.session_state.api_log[-20:]
296
 
297
  # ============================================================
298
+ # _set_status — FIX: if/else instead of ternary (Python 3.13)
299
  # ============================================================
300
  def _set_status(msg: str):
301
  st.session_state['llm_status'] = msg
 
313
  pass
314
 
315
  # ============================================================
316
+ # FORMAL TOOL CALLING SCHEMA
 
317
  # ============================================================
318
  MAYA_TOOLS = [
319
  {
 
339
  "required": ["field", "value", "reason"]
340
  }
341
  }
342
+ }
343
  ]
344
 
 
345
  _OR_TOOL_MODELS = [
346
  "google/gemini-2.0-flash-exp:free",
347
  "qwen/qwen3-32b:free",
 
356
  ]
357
 
358
  # ============================================================
359
+ # LLM API CALLERS
360
  # ============================================================
361
  def call_openrouter_tools(messages, api_key, tools=None):
362
  if not api_key:
 
483
  return text, adjustments
484
 
485
  def _call_chat_llm(messages):
 
486
  _or = st.session_state.get("openrouter_key", "")
487
  _grq = st.session_state.get("groq_key", "")
488
  if _or:
 
584
  pass
585
  return [], ""
586
 
 
 
 
587
  def _summarize_history(history, lang):
588
  if len(history) <= 8:
589
  return history, st.session_state.get('chat_summary', '')
 
610
  st.session_state.chat_summary = new_summary
611
  return recent_turns, new_summary
612
 
613
+ # Early init session state
614
  for _k, _v in [('gif_muted', False), ('llm_status', ''), ('api_log', []), ('_last_muted_sent', None)]:
615
  if _k not in st.session_state:
616
  st.session_state[_k] = _v
 
622
  st.markdown("## 🏦 SME Credit Risk AI")
623
  st.markdown("---")
624
 
625
+ # Language selector
626
  if 'lang_sel' not in st.session_state:
627
  st.session_state.lang_sel = 'id'
628
  lang = st.radio(
 
633
  )
634
  st.markdown("---")
635
 
636
+ # Music toggle
637
+ _musik_labels = {'id':'### 🎵 Musik','en':'### 🎵 Music','hi':'### 🎵 Sangeet'}
638
  st.markdown(_musik_labels.get(lang, _musik_labels['id']))
639
 
640
  _mute_labels = {
 
644
  }
645
  _ml = _mute_labels.get(lang, _mute_labels['id'])
646
  mute_label = _ml[0] if not st.session_state.gif_muted else _ml[1]
647
+ if st.button(mute_label, key="btn_mute", use_container_width=True):
648
  st.session_state.gif_muted = not st.session_state.gif_muted
649
  st.rerun()
650
 
 
655
  }
656
  st.caption(_musik_active.get(lang, _musik_active['id']))
657
 
658
+ # Media file status
659
  _media_files_map = {
660
  'crying_cat.gif': {'id':'GIF Kucing Nangis (tab SHAP)','en':'Crying Cat GIF (SHAP tab)','hi':'Rota Billa GIF (SHAP tab)'},
661
  'dance1.gif': {'id':'Dance GIF 1 (What-If)','en':'Dance GIF 1 (What-If)','hi':'Dance GIF 1 (What-If)'},
 
666
  'idk.mp3': {'id':'Musik SHAP (idk.mp3)', 'en':'SHAP Music (idk.mp3)', 'hi':'SHAP Sangeet (idk.mp3)'},
667
  'shake.mp3': {'id':'Musik What-If (shake.mp3)','en':'What-If Music (shake.mp3)','hi':'What-If Sangeet (shake.mp3)'},
668
  }
669
+ _missing = [(f, l[lang]) for f, l in _media_files_map.items() if not os.path.exists('static/' + f)]
670
+ _found = [(f, l[lang]) for f, l in _media_files_map.items() if os.path.exists('static/' + f)]
 
 
671
  _total = len(_media_files_map)
 
672
  _upload_hint = {
673
+ 'id': 'Upload file ke folder `static/` di HF Space.',
674
+ 'en': 'Upload files to the `static/` folder in your HF Space.',
675
+ 'hi': '`static/` folder mein files upload karein HF Space par.',
676
  }
 
677
  if _missing:
678
  _warn_title = {
679
  'id': '⚠️ Media belum diupload (' + str(len(_missing)) + '/' + str(_total) + ' hilang)',
 
686
  st.caption(_upload_hint.get(lang, _upload_hint['id']))
687
  if _found:
688
  _partial = {
689
+ 'id': '⏳ ' + str(len(_found)) + '/' + str(_total) + ' media siap',
690
+ 'en': '⏳ ' + str(len(_found)) + '/' + str(_total) + ' media ready',
691
+ 'hi': '⏳ ' + str(len(_found)) + '/' + str(_total) + ' media taiyaar',
692
  }
693
  st.caption(_partial.get(lang, _partial['id']))
694
  else:
695
  _all_ok = {
696
+ 'id': '✅ Semua ' + str(_total) + ' file media ditemukan — GIF & musik siap!',
697
+ 'en': '✅ All ' + str(_total) + ' media files found — GIFs & music ready!',
698
+ 'hi': '✅ Sabhi ' + str(_total) + ' media files mile — GIF & sangeet taiyaar!',
699
  }
700
  st.caption(_all_ok.get(lang, _all_ok['id']))
 
 
 
 
 
 
701
  st.markdown("---")
702
 
703
+ # API key status
704
  openrouter_key = st.session_state.openrouter_key
705
  groq_key = st.session_state.groq_key
706
  st.caption("🔑 **OpenRouter:** " + ('✅' if openrouter_key else '❌') + " **Groq:** " + ('✅' if groq_key else '❌'))
 
716
  pass
717
  st.markdown("---")
718
 
719
+ # API Tracker
720
  st.markdown("### 📡 API Tracker")
721
  _active_status = st.session_state.get('llm_status', '')
722
  if _active_status:
 
732
  st.caption(icon + " `" + entry['step'] + "` — " + entry['src'] + " (" + str(entry['ms']) + "ms)")
733
  else:
734
  st.caption("_Belum ada aktivitas API_")
735
+ if st.button("🗑️ Reset Log", key="reset_api_log", use_container_width=True):
736
  st.session_state.api_log = []
737
  st.rerun()
738
  st.markdown("---")
739
 
740
+ # Stack + footer
741
  st.markdown(T('sidebar_stack', lang))
742
  stack_items = {
743
  'id': "- 🤖 XGBoost + LightGBM + RF\n- 🔍 SHAP Eksplainabilitas\n- 💬 Narasi AI (OpenRouter + Groq)\n- 🤖 Maya AI Chat + Tool Calling\n- 🧠 Chain-of-Thought + Few-shot\n- 💾 Memory Summarization\n- 🌍 ID / EN / HI",
 
851
  ])
852
 
853
  def make_shap_png(shap_vals, feature_names):
 
854
  sv = np.array(shap_vals)
855
  names = list(feature_names)
856
  n = min(12, len(sv))
 
872
  return 'data:image/png;base64,' + b64
873
 
874
  # ============================================================
875
+ # LLM HELPERS — Narrative
876
  # ============================================================
877
  _OR_FREE_MODELS = [
878
  "google/gemini-2.0-flash-exp:free",
 
920
  except requests.exceptions.Timeout:
921
  ms = int((_time.time() - t0) * 1000)
922
  last_error = "Timeout (" + m + ")"
923
+ _log_api("Narasi", m.split('/')[-1].replace(':free',''), False, ms)
 
924
  except Exception as e:
925
  ms = int((_time.time() - t0) * 1000)
926
  last_error = m + ": " + str(e)[:100]
927
+ _log_api("Narasi", m.split('/')[-1].replace(':free',''), False, ms)
 
928
  return None, last_error
929
 
930
  def call_groq(messages, api_key):
 
960
  return None, last_error
961
 
962
  def _call_llm(messages):
 
963
  _or = st.session_state.get("openrouter_key", "")
964
  _grq = st.session_state.get("groq_key", "")
965
  last_err = None
966
  if _or:
967
  r, last_err = call_openrouter(messages, _or)
968
  if r:
969
+ return r, 'OpenRouter', None
 
970
  if _grq:
971
  r, last_err = call_groq(messages, _grq)
972
  if r:
973
+ return r, 'Groq', None
 
974
  return None, None, last_err or "No API key configured"
975
 
976
  # ============================================================
 
1112
  if not npwp:
1113
  return _t("prioritas utama: **urus NPWP** dulu di pajak.go.id","top priority: **register NPWP** at pajak.go.id","mukhya prathamikta: **NPWP register karein** pajak.go.id par")
1114
  elif dig < 40:
1115
+ return _t("**digital score " + str(dig) + "/100** adalah area paling kritis","**digital score " + str(dig) + "/100** is the most critical area","**digital score " + str(dig) + "/100** sabse mahatvapurn kshetra hai")
1116
  elif cf < 10e6:
1117
  return _t("**cash flow Rp " + str(int(cf/1e6)) + "jt/bln** perlu dioptimalkan","**cash flow Rp " + str(int(cf/1e6)) + "M/month** needs optimization","**naqad pravaah Rp " + str(int(cf/1e6)) + "M/maah** optimize zaroori hai")
1118
  elif biz < 2:
 
1120
  elif pd_pct < 20:
1121
  return _t("profil kamu sudah sangat bagus!","your profile is already excellent!","aapka parichay pehle se behtareen hai!")
1122
  else:
1123
+ return _t("beberapa area bisa diperbaiki!","several areas can be improved!","kai kshetra sudhaare jaa sakte hain!")
1124
 
1125
  # ============================================================
1126
  # AI CHAT — Maya persona
 
1190
  "- Pinjaman: Rp " + str(int(raw_input.get('loan_rp',50e6)/1e6)) + "M | Tenor: " + str(raw_input.get('duration',24)) + " bulan\n"
1191
  "- Riwayat Kredit: " + str(raw_input.get('credit_history')) + "\n\n"
1192
  "FAKTOR RISIKO UTAMA (SHAP):\n" + factors + "\n"
1193
+ + context + summary_block + cot_block + fewshot
1194
+ + "ATURAN MENJAWAB:\n"
1195
+ "1. Jawab BEBAS — tidak harus soal kredit.\n"
 
 
 
1196
  "2. Kalau relevan, selalu hubungkan ke data pemohon dengan menyebut angka aktualnya.\n"
1197
  "3. Berikan saran KONKRET dan SPESIFIK, bukan generik.\n"
1198
  "4. Maksimal 150 kata kecuali diminta lebih panjang.\n"
1199
+ "5. FORMAT TAG [ADJUST:] — embed langsung di akhir kalimat saran:\n"
1200
+ " BENAR: 'Coba naikkan digital score ke 75 [ADJUST: digital_presence_score=75] — dampaknya paling besar.'\n"
1201
+ " SALAH: Tulis tag di baris sendiri / JSON object {...}\n"
1202
+ " Field valid: digital_presence_score(1-100), business_age_years(1-20),\n"
 
 
 
 
 
1203
  " num_employees(1-50), monthly_cash_flow(angka Rp), duration(4-72), loan_rp(angka Rp)\n"
1204
  "6. JANGAN pernah balik ke template. Jawab seperti manusia cerdas.\n"
1205
  "7. Baca context percakapan sebelumnya sebelum menjawab — jaga konsistensi topik.\n"
1206
+ "8. AKHIRI response dengan kalimat lengkap.\n"
1207
  )
1208
 
1209
  messages = [{"role": "system", "content": system}]
 
1216
  if not response and tool_calls:
1217
  response = _tool_call_to_text(tool_calls, lang)
1218
 
1219
+ # Smart Keyword Fallback (only if ALL LLMs failed)
1220
  if response is None:
1221
  low = user_msg.lower()
1222
  dig = raw_input.get('digital_presence_score', 0)
 
1229
 
1230
  if any(w in low for w in ['siapa','kamu','km','who are you','nama','you are','maya','halo','hai','hei','hello','hi','perkenalan']):
1231
  response = _t(
1232
+ "Hei! Aku **Maya**, AI Credit Advisor kamu Skor PD kamu sekarang **" + str(round(pd_pct,1)) + "%** " + ('sudah bagus banget!' if pd_pct < 20 else 'masih ada ruang untuk diperbaiki.') + " Ada yang mau ditanyain?",
1233
+ "Hey! I'm **Maya**, your AI Credit Advisor. Your PD score is **" + str(round(pd_pct,1)) + "%** — " + ('looking great!' if pd_pct < 20 else 'there is room to improve.') + " Anything you want to ask?",
1234
+ "Namaste! Main **Maya** hoon. Aapka PD score **" + str(round(pd_pct,1)) + "%** hai — " + ('bahut badhiya!' if pd_pct < 20 else 'sudhaar ki gunjaish hai.') + " Kuch poochna hai?"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1235
  )
1236
+ elif any(w in low for w in ['improve','better','lower','reduce','tingkatkan','kurangi','turunkan','cara','gimana','bagaimana','naik','turun','naikkan','optimalkan']):
 
 
1237
  tips = []
1238
  if not npwp:
1239
+ tips.append(_t("1. **Urus NPWP** — daftar di pajak.go.id","1. **Register NPWP** — apply at pajak.go.id","1. **NPWP register karein** — pajak.go.id par"))
 
 
 
1240
  if dig < 75:
1241
  n = '2' if not npwp else '1'
1242
+ tips.append(_t(n + ". **Naikkan Digital Score dari " + str(dig) + " ke 75+**",n + ". **Raise Digital Score from " + str(dig) + " to 75+**",n + ". **Digital Score " + str(dig) + " se 75+ karein**"))
 
 
1243
  adjustments['digital_presence_score'] = 75
1244
  if cf < 25e6:
1245
  n = str(len(tips) + 1)
1246
+ tips.append(_t(n + ". **Optimalkan cash flow dari Rp " + str(int(cf/1e6)) + "jt ke 25jt+/bln**",n + ". **Grow cash flow from Rp " + str(int(cf/1e6)) + "M to 25M+/month**",n + ". **Naqad pravaah Rp " + str(int(cf/1e6)) + "M se 25M+ karein**"))
 
 
1247
  adjustments['monthly_cash_flow'] = 25000000
1248
  if not tips:
1249
+ tips.append(_t("Profil kamu sudah solid dengan PD " + str(round(pd_pct,1)) + "%!","Your profile is solid at PD " + str(round(pd_pct,1)) + "%!","Aapka parichay PD " + str(round(pd_pct,1)) + "% ke saath mazboot hai!"))
1250
+ response = (_t("Untuk turunkan PD dari **" + str(round(pd_pct,1)) + "%**, fokus ke:\n\n","To lower PD from **" + str(round(pd_pct,1)) + "%**, focus on:\n\n","PD **" + str(round(pd_pct,1)) + "%** kam karne ke liye:\n\n") + '\n'.join(tips[:3]))
1251
+ elif any(w in low for w in ['pinjaman','loan','kredit','berapa','ideal','rekomendasi pinjaman']):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1252
  if cf > 0:
1253
  dur_val = raw_input.get('duration', 24)
1254
  safe = cf * dur_val * 0.35
 
1256
  if cur > safe:
1257
  adjustments['loan_rp'] = safe
1258
  response = _t(
1259
+ "Cash flow kamu **Rp " + str(int(cf/1e6)) + "jt/bulan**, tenor " + str(int(dur_val)) + " bulan → pinjaman ideal max **Rp " + str(int(safe/1e6)) + "jt** (rasio 35%). Kamu sekarang Rp " + str(int(cur/1e6)) + "jt — " + ('aman!' if cur <= safe else 'melebihi batas aman!'),
1260
+ "Cash flow **Rp " + str(int(cf/1e6)) + "M/month**, " + str(int(dur_val)) + " months → ideal loan max **Rp " + str(int(safe/1e6)) + "M** (35% ratio). Current Rp " + str(int(cur/1e6)) + "M — " + ('safe!' if cur <= safe else 'exceeds safe limit!'),
1261
+ "Naqad pravaah **Rp " + str(int(cf/1e6)) + "M/maah**, " + str(int(dur_val)) + " maah → adhiktam rin **Rp " + str(int(safe/1e6)) + "M** (35%). Vartamaan Rp " + str(int(cur/1e6)) + "M — " + ('surakshit!' if cur <= safe else 'seema se adhik!')
 
 
 
 
 
 
 
 
1262
  )
1263
  else:
1264
+ response = _t("Isi cash flow bulanan di form dulu ya!","Fill in your monthly cash flow first!","Pehle maasik naqad pravaah bharein!")
1265
+ elif any(w in low for w in ['tips','bisnis','digital','marketplace','online','strategi']):
 
 
1266
  response = _t(
1267
+ "Tips bisnis UMKM buat kamu (PD " + str(round(pd_pct,1)) + "%):\n\n1. **Google Business Profile** — daftar gratis!\n2. **Aktif di marketplace** — Tokopedia/Shopee (digital score kamu " + str(dig) + "/100)\n3. **Pisah rekening** bisnis & pribadi\n4. **NPWP & SIUP** — buka akses KUR",
1268
+ "SME tips (PD " + str(round(pd_pct,1)) + "%):\n\n1. **Google Business Profile** — register free!\n2. **Active marketplace** — Tokopedia/Shopee (digital " + str(dig) + "/100)\n3. **Separate bank account** for business\n4. **NPWP & SIUP** — unlock KUR access",
1269
+ "SME tips (PD " + str(round(pd_pct,1)) + "%):\n\n1. **Google Business Profile**muft register!\n2. **Marketplace active** (digital " + str(dig) + "/100)\n3. **Alag khaata** vyapaar ke liye\n4. **NPWP & SIUP** — KUR unlock"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1270
  )
1271
  if dig < 75:
1272
  adjustments['digital_presence_score'] = 75
 
1275
  has_key = any([st.session_state.get("openrouter_key"), st.session_state.get("groq_key")])
1276
  err_note = "\n\nLLM error: " + str(_last_error)[:80] if (has_key and _last_error) else ""
1277
  response = _t(
1278
+ "PD kamu **" + str(round(pd_pct,1)) + "%** dan " + top_issue + "\n\nCoba tanya: cara turunkan skor, faktor risiko, pinjaman ideal, atau tips bisnis!" + err_note,
1279
+ "Your PD is **" + str(round(pd_pct,1)) + "%** and " + top_issue + "\n\nTry asking: how to lower score, risk factors, ideal loan, or business tips!" + err_note,
1280
+ "Aapka PD **" + str(round(pd_pct,1)) + "%** hai aur " + top_issue + "\n\nPuchein: score kaise kam karein, jokhim kaarak, ideal rin, ya vyapaar tips!" + err_note
 
 
 
 
 
 
1281
  )
1282
 
1283
  if response:
 
1289
  return response or "...", adjustments, _last_error
1290
 
1291
  # ============================================================
1292
+ # FLOATING DRAGGABLE GIF + SOUND FIXED: now actually called
 
1293
  # ============================================================
1294
  @st.cache_resource(show_spinner=False)
1295
  def _load_media_b64():
 
1312
  }
1313
 
1314
  def inject_media_manager():
1315
+ """
1316
+ Injects floating GIF + audio system into the parent Streamlit page.
1317
+ - Tab 0 (SHAP) → crying_cat.gif + idk.mp3
1318
+ - Tab 3 (What-If) → dance1-5.gif + shake.mp3
1319
+ - Other tabs → hide all GIF, pause audio
1320
+ - GIFs are draggable by user
1321
+ - Roam every 6s
1322
+ - Audio unlocked on first user click (browser autoplay policy)
1323
+ - Mute state synced every render via VOL variable
1324
+ """
1325
  media = _load_media_b64()
1326
+ has_media = any(media.get(k, '') for k in ['crying_cat', 'dance1'])
 
1327
 
1328
  muted = st.session_state.get('gif_muted', False)
1329
  vol_frac = 0.0 if muted else 0.35
1330
  vf_str = str(vol_frac)
1331
 
1332
  def _gif(gid, b64):
1333
+ if not b64:
1334
+ return ""
1335
  return (
1336
  '<div id="' + gid + '" style="'
1337
+ 'display:none;position:fixed;z-index:99999;'
1338
+ 'cursor:grab;user-select:none;'
1339
+ 'transition:left 1.1s cubic-bezier(.4,0,.2,1),'
1340
+ 'top 1.1s cubic-bezier(.4,0,.2,1);'
1341
+ 'filter:drop-shadow(0 4px 16px rgba(0,0,0,.6));'
1342
+ 'left:80px;top:80px;">'
1343
+ '<img src="' + b64 + '" style="'
1344
+ 'width:110px;height:auto;border-radius:14px;display:block;'
1345
+ 'pointer-events:none;">'
1346
  '</div>'
1347
  )
1348
 
 
1361
  dom_js = dom_str.replace('\\', '\\\\').replace('`', '\\`').replace('${', '\\${')
1362
  sa_safe = sa.replace('\\', '\\\\').replace("'", "\\'")
1363
  sw_safe = sw.replace('\\', '\\\\').replace("'", "\\'")
1364
+ has_js = 'true' if has_media else 'false'
1365
 
 
 
 
1366
  js = (
1367
  "(function(){"
1368
+ "var par=window.parent, pd=par.document;"
1369
+ "var VOL=" + vf_str + ";"
1370
+ "var HAS=" + has_js + ";"
1371
+
1372
+ # ONE-TIME INIT
1373
+ "if(!par._sme){"
1374
+ "par._sme={ct:-1,att:new WeakSet(),rt:null,vol:VOL,dragging:false};"
1375
+
1376
+ # Inject DOM once
1377
+ "if(HAS&&!pd.getElementById('sme-media-root')){"
1378
+ "var r=pd.createElement('div');r.id='sme-media-root';"
1379
+ "r.innerHTML=`" + dom_js + "`;"
1380
+ "var as2=pd.getElementById('sme-aud-shap');"
1381
+ "var aw2=pd.getElementById('sme-aud-wi');"
1382
+ "if(as2)as2.setAttribute('src','" + sa_safe + "');"
1383
+ "if(aw2)aw2.setAttribute('src','" + sw_safe + "');"
1384
+ "pd.body.appendChild(r);"
 
1385
  "}"
1386
 
1387
+ # GIF config
1388
  "var GS=['sme-gif-shap'];"
1389
  "var GW=['sme-gif-wi1','sme-gif-wi2','sme-gif-wi3','sme-gif-wi4','sme-gif-wi5'];"
1390
  "var GA=GS.concat(GW);"
1391
+ "var ZN=["
1392
+ "[0.04,0.60],[0.80,0.60],[0.42,0.68],[0.04,0.08],[0.80,0.08],"
1393
+ "[0.04,0.35],[0.80,0.35],[0.24,0.65],[0.62,0.65],[0.24,0.08],"
1394
+ "[0.62,0.08],[0.52,0.60],[0.14,0.48],[0.74,0.48]"
1395
+ "];"
1396
  "var LZ={};"
1397
 
1398
+ # Occupied positions
1399
+ "par._sme.occ=function(sk){"
1400
+ "var o=[];"
1401
+ "GA.forEach(function(id){"
1402
  "if(id===sk)return;"
1403
+ "var el=pd.getElementById(id);"
1404
  "if(!el||el.style.display==='none')return;"
1405
+ "o.push({x:el.offsetLeft,y:el.offsetTop});});"
1406
+ "return o;};"
1407
 
1408
+ # Pick random zone avoiding collisions
1409
+ "par._sme.rz=function(gid,oc){"
1410
+ "var ww=par.innerWidth,wh=par.innerHeight,sz=130;"
1411
  "var sh=ZN.slice().sort(function(){return Math.random()-0.5;});"
1412
  "var pv=LZ[gid]!=null?LZ[gid]:-1;"
1413
  "var ca=sh.filter(function(z,i){return i!==pv;});"
1414
  "if(!ca.length)ca=sh;"
1415
  "for(var i=0;i<ca.length;i++){"
1416
+ "var z=ca[i],px=Math.min(z[0]*ww,ww-sz-10),py=Math.min(z[1]*wh,wh-sz-10);"
 
1417
  "var ok=true;"
1418
  "for(var j=0;j<oc.length;j++){"
1419
  "var dx=px-oc[j].x,dy=py-oc[j].y;"
1420
+ "if(Math.sqrt(dx*dx+dy*dy)<160){ok=false;break;}}"
1421
  "if(ok){LZ[gid]=ZN.indexOf(z);return{x:px,y:py};}}"
1422
  "LZ[gid]=ZN.indexOf(ca[0]);"
1423
+ "return{x:Math.min(ca[0][0]*ww,ww-sz-10),"
1424
+ "y:Math.min(ca[0][1]*wh,wh-sz-10)};};"
1425
 
1426
+ # Move one GIF
1427
+ "par._sme.mv=function(gid){"
1428
+ "if(par._sme.dragging)return;"
1429
+ "var el=pd.getElementById(gid);"
1430
  "if(!el||el.style.display==='none')return;"
1431
+ "var p=par._sme.rz(gid,par._sme.occ(gid));"
1432
  "el.style.left=p.x+'px';el.style.top=p.y+'px';"
1433
  "el.style.right='auto';el.style.bottom='auto';};"
1434
 
1435
+ # Play audio
1436
+ "par._sme.pa=function(id){"
1437
+ "var a=pd.getElementById(id);"
1438
+ "if(!a)return;"
1439
+ "a.volume=par._sme.vol;"
1440
+ "var p=a.play();"
1441
+ "if(p&&p.catch){p.catch(function(){"
1442
+ "par._sme.pendingAud=id;});}};"
1443
+
1444
+ # Stop all
1445
+ "par._sme.stop=function(){"
1446
  "GA.forEach(function(id){"
1447
+ "var el=pd.getElementById(id);if(el)el.style.display='none';});"
1448
  "['sme-aud-shap','sme-aud-wi'].forEach(function(id){"
1449
+ "var a=pd.getElementById(id);"
1450
+ "if(a){a.pause();a.currentTime=0;}});"
1451
+ "par._sme.pendingAud=null;};"
1452
+
1453
+ # Show SHAP (tab 0)
1454
+ "par._sme.showS=function(){"
1455
+ "par._sme.stop();"
1456
+ "var el=pd.getElementById('sme-gif-shap');if(!el)return;"
1457
+ "var p=par._sme.rz('sme-gif-shap',[]);"
1458
  "el.style.left=p.x+'px';el.style.top=p.y+'px';"
1459
  "el.style.right='auto';el.style.bottom='auto';"
1460
  "el.style.display='block';"
1461
+ "par._sme.pa('sme-aud-shap');};"
1462
 
1463
+ # Show What-If dancers (tab 3)
1464
+ "par._sme.showW=function(){"
1465
+ "par._sme.stop();"
1466
+ "var ww=par.innerWidth,wh=par.innerHeight,sz=110;"
1467
  "var sp=[0.03,0.22,0.41,0.60,0.79];"
1468
  "GW.forEach(function(gid,i){"
1469
+ "var el=pd.getElementById(gid);if(!el)return;"
1470
  "el.style.left=Math.min(sp[i]*ww,ww-sz-10)+'px';"
1471
  "el.style.top=Math.min(0.68*wh,wh-sz-10)+'px';"
1472
  "el.style.right='auto';el.style.bottom='auto';"
1473
  "el.style.display='block';"
1474
  "LZ[gid]=-1;});"
1475
+ "par._sme.pa('sme-aud-wi');};"
1476
+
1477
+ # Apply tab state
1478
+ "par._sme.apT=function(idx){"
1479
+ "if(idx===par._sme.ct)return;"
1480
+ "par._sme.ct=idx;"
1481
+ "if(idx===0)par._sme.showS();"
1482
+ "else if(idx===3)par._sme.showW();"
1483
+ "else par._sme.stop();};"
1484
+
1485
+ # Get current active tab index
1486
+ "par._sme.tidx=function(){"
1487
+ "var all=pd.querySelectorAll('[data-baseweb=\"tab\"]');"
1488
+ "if(!all||!all.length)all=pd.querySelectorAll('[role=\"tab\"]');"
1489
+ "if(!all||!all.length)return -1;"
1490
  "for(var i=0;i<all.length;i++){"
1491
  "if(all[i].getAttribute('aria-selected')==='true')return i;}"
1492
  "return 0;};"
1493
 
1494
+ # DRAG support
1495
+ "par._sme.initDrag=function(el){"
1496
+ "if(el.dataset.drag)return;"
1497
+ "el.dataset.drag='1';"
1498
+ "var ox,oy,sx,sy;"
1499
+ "el.addEventListener('mousedown',function(e){"
1500
+ "e.preventDefault();"
1501
+ "par._sme.dragging=true;"
1502
+ "el.style.transition='none';"
1503
+ "el.style.cursor='grabbing';"
1504
+ "sx=e.clientX;sy=e.clientY;"
1505
+ "ox=el.offsetLeft;oy=el.offsetTop;"
1506
+ "pd.addEventListener('mousemove',mmv);"
1507
+ "pd.addEventListener('mouseup',mup);});"
1508
+ "function mmv(e){"
1509
+ "var dx=e.clientX-sx,dy=e.clientY-sy;"
1510
+ "el.style.left=(ox+dx)+'px';el.style.top=(oy+dy)+'px';"
1511
+ "el.style.right='auto';el.style.bottom='auto';}"
1512
+ "function mup(){"
1513
+ "par._sme.dragging=false;"
1514
+ "el.style.cursor='grab';"
1515
+ "el.style.transition='left 1.1s cubic-bezier(.4,0,.2,1),"
1516
+ "top 1.1s cubic-bezier(.4,0,.2,1)';"
1517
+ "pd.removeEventListener('mousemove',mmv);"
1518
+ "pd.removeEventListener('mouseup',mup);"
1519
+ "if(par._sme.pendingAud){par._sme.pa(par._sme.pendingAud);par._sme.pendingAud=null;}}"
1520
+ "};"
1521
 
1522
+ # Init drag on all GIFs
1523
+ "GA.forEach(function(id){"
1524
+ "var el=pd.getElementById(id);if(el)par._sme.initDrag(el);});"
1525
+
1526
+ # Audio unlock on first click
1527
+ "if(!par._sme.audioUnlocked){"
1528
+ "par._sme.audioUnlocked=false;"
1529
+ "pd.addEventListener('click',function _unlock(){"
1530
+ "par._sme.audioUnlocked=true;"
1531
+ "pd.removeEventListener('click',_unlock);"
1532
+ "if(par._sme.pendingAud){"
1533
+ "par._sme.pa(par._sme.pendingAud);"
1534
+ "par._sme.pendingAud=null;}},true);}"
1535
+
1536
+ # Roam every 6 seconds
1537
+ "if(par._sme.rt)par.clearInterval(par._sme.rt);"
1538
+ "par._sme.rt=par.setInterval(function(){"
1539
+ "GA.forEach(par._sme.mv);"
1540
+ "},6000);"
1541
+
1542
+ "}" # END first-time init
1543
+
1544
+ # EVERY RENDER: update volume + re-attach tab clicks
1545
+ "par._sme.vol=VOL;"
1546
  "['sme-aud-shap','sme-aud-wi'].forEach(function(id){"
1547
+ "var a=pd.getElementById(id);if(a)a.volume=VOL;});"
1548
 
1549
+ # Re-attach drag to any new GIF elements
1550
+ "var GA2=['sme-gif-shap','sme-gif-wi1','sme-gif-wi2','sme-gif-wi3','sme-gif-wi4','sme-gif-wi5'];"
1551
+ "GA2.forEach(function(id){"
1552
+ "var el=pd.getElementById(id);"
1553
+ "if(el&&par._sme.initDrag)par._sme.initDrag(el);});"
1554
+
1555
+ # Re-attach tab click listeners
1556
  "function addC(){"
1557
+ "var tabs=pd.querySelectorAll('[data-baseweb=\"tab\"]');"
1558
+ "if(!tabs||tabs.length<2)tabs=pd.querySelectorAll('[role=\"tab\"]');"
1559
  "if(!tabs||tabs.length<2)return false;"
1560
  "tabs.forEach(function(tab,i){"
1561
+ "if(par._sme.att.has(tab))return;"
1562
+ "par._sme.att.add(tab);"
1563
  "tab.addEventListener('click',function(){"
1564
+ "par.setTimeout(function(){par._sme.apT(par._sme.tidx());},150);});});"
1565
  "return true;}"
1566
 
1567
+ "var tr=0,pl=par.setInterval(function(){"
1568
  "tr++;"
1569
  "if(addC()){"
1570
+ "var ci=par._sme.tidx();"
1571
+ "if(ci!==par._sme.ct){par._sme.ct=-1;par._sme.apT(ci);}"
1572
+ "par.clearInterval(pl);"
1573
+ "par.setInterval(function(){addC();},2000);}"
1574
+ "if(tr>80)par.clearInterval(pl);"
1575
+ "},150);"
1576
 
1577
  "})();"
1578
  )
1579
 
1580
+ html = '<!DOCTYPE html><html><head></head><body><script>' + js + '</script></body></html>'
1581
+ components.html(html, height=1, scrolling=False)
1582
+
1583
 
1584
  # ============================================================
1585
  # HEADER
1586
  # ============================================================
 
 
1587
  st.markdown(
1588
+ '<div class="header-wrap">'
1589
+ '<h1>' + T("header_title", lang) + '</h1>'
1590
+ '<p>' + T("header_sub", lang) + '</p>'
1591
+ '<span class="badge b-blue">XGBoost + LightGBM + RF</span>'
1592
+ '<span class="badge b-green">SHAP XAI</span>'
1593
+ '<span class="badge b-yellow">Maya AI Chat</span>'
1594
+ '</div>',
 
 
 
 
 
1595
  unsafe_allow_html=True
1596
  )
1597
 
1598
  # ============================================================
1599
+ # FORM
1600
  # ============================================================
 
1601
  with st.form("credit_form"):
1602
+ st.markdown(T('form_title', lang))
1603
+ col_a, col_b, col_c = st.columns(3)
1604
+
1605
+ with col_a:
1606
  st.markdown(T('form_loan', lang))
1607
+ loan_rp = st.number_input(T('f_loan_amt',lang), min_value=1_000_000, max_value=5_000_000_000,
1608
+ value=50_000_000, step=1_000_000)
1609
+ lgd_input = st.slider(T('f_lgd',lang), 0.0, 1.0, 0.40, 0.05)
1610
+ duration = st.slider(T('f_duration',lang), 4, 72, 24)
1611
+ credit_amount = st.number_input(T('f_credit_amt',lang), min_value=100, max_value=20000,
1612
+ value=1500, step=100)
1613
+ purpose_sel = st.selectbox(
1614
+ T('f_purpose',lang),
1615
+ PURPOSE_OPTS,
1616
+ format_func=lambda x: PURPOSE_LABELS[x][lang]
1617
+ )
1618
+ age = st.slider(T('f_age',lang), 18, 75, 35)
1619
+
1620
+ with col_b:
1621
  st.markdown(T('form_financial', lang))
1622
+ checking_sel = st.selectbox(
1623
+ T('f_checking',lang),
1624
+ CHECKING_OPTS,
1625
+ format_func=lambda x: CHECKING_LABELS[x][lang]
1626
+ )
1627
+ savings_sel = st.selectbox(
1628
+ T('f_savings',lang),
1629
+ SAVINGS_OPTS,
1630
+ format_func=lambda x: SAVINGS_LABELS[x][lang]
1631
+ )
1632
+ credit_hist_sel = st.selectbox(
1633
+ T('f_credit_hist',lang),
1634
+ CREDIT_H_OPTS,
1635
+ format_func=lambda x: CREDIT_H_LABELS[x][lang]
1636
+ )
1637
+ employment_sel = st.selectbox(
1638
+ T('f_employment',lang),
1639
+ EMPLOY_OPTS,
1640
+ format_func=lambda x: EMPLOY_LABELS[x][lang]
1641
+ )
1642
+ housing_sel = st.selectbox(
1643
+ T('f_housing',lang),
1644
+ HOUSING_OPTS,
1645
+ format_func=lambda x: HOUSING_LABELS[x][lang]
1646
+ )
1647
+ installment_rate = st.slider(T('f_installment',lang), 1, 4, 2)
1648
+
1649
+ with col_c:
1650
  st.markdown(T('form_sme', lang))
1651
  digital_score = st.slider(T('f_digital',lang), 1, 100, 50)
1652
+ has_social = st.checkbox(T('f_social',lang), value=True)
1653
+ ecomm_volume = st.number_input(T('f_ecomm',lang), min_value=0, value=5_000_000, step=500_000)
1654
+ has_npwp = st.checkbox(T('f_npwp',lang), value=True)
1655
+ has_siup = st.checkbox(T('f_siup',lang), value=True)
1656
+ biz_age = st.slider(T('f_biz_age',lang), 1, 20, 5)
1657
+ cash_flow = st.number_input(T('f_cashflow',lang), min_value=0, value=15_000_000, step=500_000)
1658
+ num_employees = st.slider(T('f_employees',lang), 1, 50, 5)
 
1659
 
1660
+ submitted = st.form_submit_button(T('f_submit', lang), use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1661
 
1662
  # ============================================================
1663
+ # PROCESS FORM SUBMISSION
1664
  # ============================================================
1665
  if submitted:
1666
  raw = {
1667
+ 'duration': duration,
1668
+ 'credit_amount': credit_amount,
1669
+ 'installment_commitment': installment_rate,
1670
+ 'age': age,
1671
+ 'checking_status': checking_sel,
1672
+ 'credit_history': credit_hist_sel,
1673
+ 'purpose': purpose_sel,
1674
+ 'savings_status': savings_sel,
1675
+ 'employment': employment_sel,
1676
+ 'housing': housing_sel,
1677
+ 'digital_presence_score': digital_score,
1678
+ 'has_social_media': int(has_social),
1679
+ 'ecommerce_volume': ecomm_volume,
1680
+ 'has_npwp': int(has_npwp),
1681
+ 'has_siup': int(has_siup),
1682
+ 'business_age_years': float(biz_age),
1683
+ 'monthly_cash_flow': float(cash_flow),
1684
+ 'num_employees': num_employees,
1685
+ 'loan_rp': float(loan_rp),
1686
  }
1687
+ with st.spinner(T('spinner', lang)):
1688
+ X_proc = preprocess(raw, scaler, feature_names)
1689
+ pd_score = float(ensemble.predict_proba(X_proc)[0][1])
1690
+ result = risk_result(pd_score, loan_rp, lgd_input)
 
 
 
 
1691
 
1692
+ try:
1693
+ sv = explainer.shap_values(X_proc)
1694
+ sv = sv[1] if isinstance(sv, list) else sv
1695
+ sv = np.array(sv).flatten()
1696
+ except Exception:
1697
+ sv = np.zeros(len(feature_names))
1698
 
1699
+ narrative, llm_src = get_narrative(sv, feature_names, result, lang, raw)
 
 
1700
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1701
  st.session_state.result = result
1702
+ st.session_state.shap_vals = sv
1703
  st.session_state.raw_input = raw
1704
  st.session_state.narrative = narrative
1705
  st.session_state.llm_src = llm_src
1706
  st.session_state.narrative_lang = lang
1707
+ st.session_state.shap_png = make_shap_png(sv, feature_names)
1708
+ # Reset What-If sliders to new values
1709
+ st.session_state.wi_dig = digital_score
1710
+ st.session_state.wi_biz = biz_age
1711
+ st.session_state.wi_emp = num_employees
1712
+ st.session_state.wi_cash = int(cash_flow // 1e6)
1713
+ st.session_state.wi_dur = duration
1714
+ st.session_state.wi_loan = int(loan_rp // 1e6)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1715
 
1716
  # ============================================================
1717
+ # Pull session state → local vars (used by tabs below)
1718
  # ============================================================
1719
+ result = st.session_state.get('result')
1720
+ shap_vals = st.session_state.get('shap_vals', np.zeros(len(feature_names) if feature_names else 1))
1721
+ raw_input = st.session_state.get('raw_input', {})
1722
+ narrative = st.session_state.get('narrative', '')
1723
+ llm_src = st.session_state.get('llm_src', '')
 
 
 
 
1724
 
1725
+ # ============================================================
1726
+ # KPI BANNER (only shown after submission)
1727
+ # ============================================================
1728
+ if result:
1729
+ st.markdown(
1730
+ '<div class="risk-banner ' + result['css'] + '">'
1731
+ + result['cat'][lang] + '</div>',
1732
+ unsafe_allow_html=True
1733
+ )
1734
+ k1, k2, k3, k4 = st.columns(4)
1735
+ with k1:
1736
  st.markdown(
1737
+ '<div class="kpi-box">'
1738
+ '<div class="kpi-lbl">' + T('kpi_pd', lang) + '</div>'
1739
+ '<div class="kpi-val">' + str(round(result['pd']*100,1)) + '%</div>'
1740
+ '<div class="kpi-sub">' + T('kpi_pd_sub', lang) + '</div>'
1741
+ '</div>', unsafe_allow_html=True
1742
  )
1743
+ with k2:
1744
+ st.markdown(
1745
+ '<div class="kpi-box">'
1746
+ '<div class="kpi-lbl">' + T('kpi_el', lang) + '</div>'
1747
+ '<div class="kpi-val">Rp ' + str(round(result['el']/1e6,1)) + 'jt</div>'
1748
+ '<div class="kpi-sub">' + T('kpi_el_sub', lang) + '</div>'
1749
+ '</div>', unsafe_allow_html=True
1750
+ )
1751
+ with k3:
1752
+ st.markdown(
1753
+ '<div class="kpi-box">'
1754
+ '<div class="kpi-lbl">' + T('kpi_lgd', lang) + '</div>'
1755
+ '<div class="kpi-val">' + str(int(result['lgd']*100)) + '%</div>'
1756
+ '<div class="kpi-sub">' + T('kpi_lgd_sub', lang) + '</div>'
1757
+ '</div>', unsafe_allow_html=True
1758
+ )
1759
+ with k4:
1760
+ st.markdown(
1761
+ '<div class="kpi-box">'
1762
+ '<div class="kpi-lbl">' + T('kpi_ead', lang) + '</div>'
1763
+ '<div class="kpi-val">Rp ' + str(int(result['ead']/1e6)) + 'jt</div>'
1764
+ '<div class="kpi-sub">' + T('kpi_ead_sub', lang) + '</div>'
1765
+ '</div>', unsafe_allow_html=True
1766
+ )
1767
+ st.markdown("<br>", unsafe_allow_html=True)
1768
 
1769
+ # ============================================================
1770
+ # TAB LOADING PROGRESS
1771
+ # ============================================================
1772
  _tab_load_steps = {
1773
+ 'id': ['🔍 Menyiapkan SHAP...', '💬 Menyiapkan Narasi AI...', '🤖 Menyiapkan Chat...',
1774
+ '🎮 Menyiapkan What-If...', '📐 Menyiapkan Formula...', '✅ Semua tab siap!'],
1775
+ 'en': ['🔍 Preparing SHAP...', '💬 Preparing AI Narrative...', '🤖 Preparing Chat...',
1776
+ '🎮 Preparing What-If...', '📐 Preparing Formula...', '✅ All tabs ready!'],
1777
+ 'hi': ['🔍 SHAP taiyaar...', '💬 AI Vivarana taiyaar...', '🤖 Chat taiyaar...',
1778
+ '🎮 What-If taiyaar...', '📐 Sutra taiyaar...', '✅ Sabhi tab taiyaar!'],
1779
  }
1780
+ _tls = _tab_load_steps.get(lang, _tab_load_steps['id'])
1781
+ _tab_prog_ph = st.empty()
 
1782
 
1783
  def _tab_step(n):
1784
+ try:
1785
  pct = int(n / 5 * 100)
1786
  _tab_prog_ph.progress(pct, text=_tls[n])
1787
+ except Exception:
1788
+ pass
1789
 
1790
  _tab_step(0)
1791
 
1792
+ # ============================================================
1793
  # TABS
1794
+ # ============================================================
1795
  t1, t2, t3, t4, t5 = st.tabs([
1796
  T('tab_shap',lang), T('tab_narrative',lang),
1797
  T('tab_chat',lang), T('tab_whatif',lang), T('tab_formula',lang)
1798
  ])
1799
 
1800
+ # ── TAB 1: SHAP ──────────────────────────────────────────────
1801
  with t1:
1802
  _tab_step(0)
1803
  st.markdown('<div class="sec-title">' + T("shap_title",lang) + '</div>', unsafe_allow_html=True)
1804
+
1805
+ if result is None:
1806
+ st.info("⬆️ " + {'id':'Submit form di atas untuk melihat analisis SHAP.',
1807
+ 'en':'Submit the form above to see SHAP analysis.',
1808
+ 'hi':'SHAP vishleshan dekhne ke liye upar form submit karein.'}[lang])
 
 
 
1809
  else:
1810
+ _shap_all_zero = np.all(shap_vals == 0)
1811
+ if _shap_all_zero:
1812
+ st.warning(
1813
+ "⚠️ **SHAP tidak tersedia** pastikan `model/xgb_model.pkl` ada di HF Space.\n\n"
1814
+ "Skor PD dan narasi tetap valid (menggunakan ensemble model)."
1815
+ )
1816
+ else:
1817
+ if st.session_state.get("shap_png"):
1818
+ _spng = st.session_state.shap_png
1819
+ if isinstance(_spng, str) and _spng.startswith('data:'):
1820
+ st.markdown('<img src="' + _spng + '" style="width:100%;border-radius:8px;">', unsafe_allow_html=True)
1821
+ elif _spng:
1822
+ st.image(_spng, use_container_width=True)
1823
+ st.caption(T('shap_caption', lang))
1824
+ top_idx = np.argsort(np.abs(shap_vals))[-10:][::-1]
1825
+ st.dataframe(pd.DataFrame({
1826
+ T('shap_col_feature',lang): [feature_names[i] for i in top_idx],
1827
+ 'SHAP': [round(shap_vals[i],4) for i in top_idx],
1828
+ T('shap_col_impact',lang): [
1829
+ T('shap_risk_up',lang) if shap_vals[i] > 0 else T('shap_risk_down',lang)
1830
+ for i in top_idx
1831
+ ],
1832
+ }), use_container_width=True, hide_index=True)
1833
+
1834
+ # ── TAB 2: NARRATIVE ─────────────────────────────────────────
1835
  with t2:
1836
  _tab_step(1)
1837
  flag = {'id':'🇮🇩','en':'🇬🇧','hi':'🇮🇳'}[lang]
1838
  st.markdown('<div class="sec-title">' + T("narr_title",lang) + ' ' + flag + '</div>', unsafe_allow_html=True)
1839
+
1840
+ if result is None:
1841
+ st.info("⬆️ " + {'id':'Submit form di atas untuk melihat narasi AI.',
1842
+ 'en':'Submit the form above to see the AI narrative.',
1843
+ 'hi':'AI vivarana dekhne ke liye upar form submit karein.'}[lang])
1844
  else:
1845
+ st.caption(T('narr_source',lang) + ": " + (llm_src if llm_src else "—"))
1846
+ if narrative and narrative.strip():
1847
+ fmt = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', narrative.replace('\n','<br>'))
1848
+ st.markdown('<div class="narrative-box">' + fmt + '</div>', unsafe_allow_html=True)
1849
+ else:
1850
+ narrative_fb, src_fb = get_narrative(shap_vals, feature_names, result, lang, raw_input)
1851
+ st.session_state.narrative = narrative_fb
1852
+ st.session_state.llm_src = src_fb
1853
+ fmt = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', narrative_fb.replace('\n','<br>'))
1854
+ st.markdown('<div class="narrative-box">' + fmt + '</div>', unsafe_allow_html=True)
1855
  _or_key = st.session_state.get("openrouter_key","")
1856
  _grq_key = st.session_state.get("groq_key","")
1857
+ with st.expander("🔧 LLM Debug Info"):
1858
  st.code(
1859
  "OR key : " + ('OK ' + _or_key[:12] + '...' if _or_key else 'missing') + "\n"
1860
  "Groq key : " + ('OK ' + _grq_key[:12] + '...' if _grq_key else 'missing') + "\n"
1861
+ "Source : " + str(llm_src) + "\n"
1862
  "OR model : " + _OR_FREE_MODELS[0] + "\n"
1863
  "Groq mdl : " + _GROQ_FREE_MODELS[0]
1864
  )
1865
 
1866
+ # ── TAB 3: AI CHAT ────────────────────────────────────────────
1867
  with t3:
1868
  _tab_step(2)
1869
  st.markdown('<div class="sec-title">' + T("chat_title",lang) + '</div>', unsafe_allow_html=True)
1870
 
1871
+ if result is None:
1872
+ st.info("⬆️ " + {'id':'Submit form di atas untuk chat dengan Maya.',
1873
+ 'en':'Submit the form above to chat with Maya.',
1874
+ 'hi':'Maya se baat karne ke liye upar form submit karein.'}[lang])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1875
  else:
1876
+ if st.session_state.get('chat_summary'):
1877
+ n_turns = len(st.session_state.chat_history)
1878
+ lbl = {
1879
+ 'id': 'Memory aktif - ' + str(n_turns) + ' pesan tersimpan',
1880
+ 'en': 'Memory active - ' + str(n_turns) + ' messages saved',
1881
+ 'hi': 'Memory active - ' + str(n_turns) + ' sandesh save hue',
1882
+ }
1883
+ st.markdown('<div class="memory-badge">' + lbl.get(lang, lbl["en"]) + '</div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1884
 
1885
+ chips = [T('chat_chip1',lang), T('chat_chip2',lang), T('chat_chip3',lang), T('chat_chip4',lang)]
1886
+ chip_cols = st.columns(len(chips))
1887
+ chip_clicked = None
1888
+ for i, (col, chip) in enumerate(zip(chip_cols, chips)):
1889
+ with col:
1890
+ if st.button(chip, key="chip_" + str(i), use_container_width=True):
1891
+ chip_clicked = chip
1892
+ st.markdown("---")
1893
+
1894
+ if st.session_state.chat_history:
1895
+ html_chat = ""
1896
+ for msg in st.session_state.chat_history:
1897
+ content = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>',
1898
+ msg['content'].replace('\n','<br>'))
1899
+ cls = "chat-bubble-user" if msg['role'] == 'user' else "chat-bubble-ai"
1900
+ icon = "👤" if msg['role'] == 'user' else "🤖"
1901
+ html_chat += '<div class="' + cls + '">' + icon + ' ' + content + '</div>'
1902
+ st.markdown(html_chat, unsafe_allow_html=True)
1903
+ else:
1904
+ st.info("👋 " + T('chat_welcome', lang))
1905
+
1906
+ user_input = st.chat_input(T('chat_input', lang))
1907
+ to_process = chip_clicked or user_input
1908
+ if to_process:
1909
+ st.session_state.chat_history.append({'role':'user','content':to_process})
1910
+ _chat_status_ph = st.empty()
1911
+ _spinner_label = {'id':'🤔 Maya sedang berpikir...','en':'🤔 Maya is thinking...','hi':'🤔 Maya soch rahi hai...'}.get(lang,'🤔 Maya sedang berpikir...')
1912
+ with st.spinner(_spinner_label):
1913
+ _chat_status_ph.info(
1914
+ {'id':'⏳ Menghubungi LLM — bisa 5–15 detik...','en':'⏳ Connecting to LLM — may take 5–15 seconds...','hi':'⏳ LLM se connect ho raha hai — 5–15 second lag sakte hain...'}[lang],
1915
+ icon="⏳"
1916
+ )
1917
+ ai_resp, adjustments, _debug_err = get_chat_response(
1918
+ to_process, st.session_state.chat_history,
1919
+ result, raw_input, shap_vals, feature_names, lang,
1920
+ rag_index=rag_index
1921
+ )
1922
+ _chat_status_ph.empty()
1923
+ if _debug_err and any([st.session_state.get("openrouter_key"), st.session_state.get("groq_key")]):
1924
+ st.warning("Semua LLM gagal, menggunakan Smart Fallback. Error: " + str(_debug_err)[:100])
1925
+ st.session_state.chat_history.append({'role':'assistant','content':ai_resp})
1926
+ _save_chat_memory(st.session_state.chat_history, st.session_state.get('chat_summary',''))
1927
+ adj_map = {
1928
+ 'digital_presence_score': 'wi_dig', 'business_age_years': 'wi_biz',
1929
+ 'num_employees': 'wi_emp', 'monthly_cash_flow': 'wi_cash',
1930
+ 'duration': 'wi_dur', 'loan_rp': 'wi_loan',
1931
+ }
1932
+ if adjustments:
1933
+ for field, val in adjustments.items():
1934
+ if field in adj_map:
1935
+ st.session_state[adj_map[field]] = val
1936
+ st.success("💡 " + T('chat_updated', lang))
1937
  st.rerun()
1938
 
1939
+ if st.session_state.chat_history:
1940
+ if st.button(T('chat_clear', lang)):
1941
+ st.session_state.chat_history = []
1942
+ st.session_state.chat_summary = ''
1943
+ _save_chat_memory([], '')
1944
+ st.rerun()
1945
+
1946
+ # ── TAB 4: WHAT-IF ────────────────────────────────────────────
1947
  with t4:
1948
  _tab_step(3)
1949
+
1950
  @st.fragment
1951
  def render_whatif():
1952
  _lang = st.session_state.get("lang_sel","id")
 
2051
  with st.expander(T('wi_tips_title', _lang)):
2052
  tips = []
2053
  if wi_dig < 70:
2054
+ tips.append(_t("📱 Naikkan Digital Score ke 70+ — marketplace & Google Business","📱 Raise Digital Score to 70+ — marketplace & Google Business","📱 Digital Score 70+ karein — marketplace & Google Business"))
 
 
2055
  if wi_cash < 20:
2056
+ tips.append(_t("💵 Target cash flow Rp 20jt+/bulan","💵 Target Rp 20M+/month cash flow","💵 Naqad pravaah Rp 20M+/maah target"))
 
 
2057
  if wi_biz < 3:
2058
+ tips.append(_t("🏢 Bisnis < 3 thn lebih berisiko — bangun track record","🏢 Business < 3 yr is riskier — build track record","🏢 3 saal se kam vyapaar — track record banaen"))
 
 
2059
  if wi_emp < 5:
2060
+ tips.append(_t("👥 Tambah karyawan = skala bisnis lebih sehat","👥 More employees signals healthy scale","👥 Zyada karmachaaree = swasth paimaana"))
 
 
2061
  if not tips:
2062
+ tips.append(_t("🌟 Profil sudah optimal!","🌟 Profile already well-optimized!","🌟 Parichay pehle se anukoolit!"))
 
 
2063
  for tip in tips:
2064
  st.markdown(tip)
2065
 
 
2068
 
2069
  render_whatif()
2070
 
2071
+ # ── TAB 5: FORMULA ───────────────────────────────────────────
2072
  with t5:
2073
  _tab_step(4)
2074
  st.markdown('<div class="sec-title">' + T("form_formula_title",lang) + '</div>', unsafe_allow_html=True)
2075
+
2076
+ if result is None:
2077
+ st.info("⬆️ " + {'id':'Submit form di atas untuk melihat breakdown formula.',
2078
+ 'en':'Submit the form above to see the formula breakdown.',
2079
+ 'hi':'Formula dekhne ke liye upar form submit karein.'}[lang])
2080
+ else:
2081
+ st.latex(r"EL = PD \times LGD \times EAD")
2082
+ st.latex(
2083
+ "EL = " + str(round(result['pd'],4)) + r" \times " + str(round(result['lgd'],2))
2084
+ + r" \times Rp\," + "{:,}".format(int(result['ead']))
2085
+ + r" = Rp\," + "{:,}".format(int(result['el']))
 
 
 
 
 
 
 
 
 
 
 
2086
  )
2087
+ cf1, cf2 = st.columns(2)
2088
+ with cf1:
2089
+ st.markdown(T('formula_def', lang))
2090
+ with cf2:
2091
+ cat_lbl = (
2092
+ T('formula_low',lang) if result['pd'] < .2 else
2093
+ T('formula_medium',lang) if result['pd'] < .5 else
2094
+ T('formula_high',lang)
2095
+ )
2096
+ st.markdown(
2097
+ "| " + T('formula_komponen',lang) + " | " + T('formula_nilai',lang) + " |\n"
2098
+ "|--|--|\n"
2099
+ "| PD | " + str(round(result['pd']*100,1)) + "% (" + cat_lbl + ") |\n"
2100
+ "| LGD | " + str(int(result['lgd']*100)) + "% |\n"
2101
+ "| EAD | Rp " + str(int(result['ead']/1e6)) + "jt |\n"
2102
+ "| **EL** | **Rp " + str(round(result['el']/1e6,2)) + "jt** |"
2103
+ )
2104
+ _log_api("Formula", "Local", True, 0)
2105
 
2106
+ # ============================================================
2107
+ # CLEAR PROGRESS + INJECT MEDIA MANAGER
2108
+ # ← FIX: inject_media_manager() is NOW CALLED HERE
2109
+ # (was defined but never called in original code)
2110
+ # Called every render so mute state syncs correctly
2111
+ # ============================================================
2112
  _tab_step(5)
2113
  _tab_prog_ph.empty()
2114
+
2115
+ # Inject GIF + audio system (runs after all tabs are rendered)
2116
+ inject_media_manager()
2117
 
2118
  # ============================================================
2119
  # DOWNLOAD REPORT
2120
  # ============================================================
2121
+ if result:
2122
+ st.markdown("---")
2123
+ report_txt = (
2124
+ "SME CREDIT RISK REPORT 1na37 AI · Batch 10\n" + "="*50 + "\n"
2125
+ + T('kpi_pd',lang) + ": " + str(round(result['pd']*100,1)) + "% → " + result['cat'][lang] + "\n"
2126
+ + T('kpi_el',lang) + ": Rp " + "{:,}".format(int(result['el'])) + "\n"
2127
+ "LGD: " + str(int(result['lgd']*100)) + "% (Basel II)\n"
2128
+ "EAD: Rp " + "{:,}".format(int(result['ead'])) + "\n"
2129
+ + "="*50 + "\n"
2130
+ + T('narr_title',lang) + "\n" + narrative + "\n"
2131
+ + "="*50 + "\n"
2132
+ "TOP SHAP\n" + shap_summary(shap_vals, feature_names, 5) + "\n"
2133
+ + "="*50 + "\n"
2134
+ "DISCLAIMER: Educational use only. Not financial advice."
2135
+ )
2136
+ st.download_button(
2137
+ T('download_btn', lang),
2138
+ data=report_txt,
2139
+ file_name=T('download_file', lang),
2140
+ mime="text/plain"
2141
+ )