1na37 commited on
Commit
f20dab1
ยท
verified ยท
1 Parent(s): c43aa13

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +423 -802
app.py CHANGED
@@ -1,15 +1,13 @@
1
  # ============================================================
2
- # ๐Ÿฆ SME Credit Risk Assessment โ€” FINAL (FIXED)
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
@@ -32,7 +30,7 @@ st.set_page_config(
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,9 +129,7 @@ TRANSLATIONS = {
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,6 +153,10 @@ TRANSLATIONS = {
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,7 +164,7 @@ 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,7 +220,7 @@ HOUSING_LABELS = {
220
  LANG_LABELS = {'id':'๐Ÿ‡ฎ๐Ÿ‡ฉ Bahasa Indonesia','en':'๐Ÿ‡ฌ๐Ÿ‡ง English','hi':'๐Ÿ‡ฎ๐Ÿ‡ณ Hindi (Roman)'}
221
 
222
  # ============================================================
223
- # CSS
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 โ€” FIX: if/else instead of ternary (Python 3.13)
299
  # ============================================================
300
  def _set_status(msg: str):
301
  st.session_state['llm_status'] = msg
@@ -313,7 +313,7 @@ def _set_status(msg: str):
313
  pass
314
 
315
  # ============================================================
316
- # FORMAL TOOL CALLING SCHEMA
317
  # ============================================================
318
  MAYA_TOOLS = [
319
  {
@@ -342,6 +342,7 @@ MAYA_TOOLS = [
342
  }
343
  ]
344
 
 
345
  _OR_TOOL_MODELS = [
346
  "google/gemini-2.0-flash-exp:free",
347
  "qwen/qwen3-32b:free",
@@ -356,7 +357,7 @@ _GROQ_TOOL_MODELS = [
356
  ]
357
 
358
  # ============================================================
359
- # LLM API CALLERS
360
  # ============================================================
361
  def call_openrouter_tools(messages, api_key, tools=None):
362
  if not api_key:
@@ -452,6 +453,7 @@ def _clean_response(text: str) -> tuple:
452
  return text, {}
453
  adjustments = {}
454
  text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
 
455
  json_pattern = re.compile(r'\{[^{}]*"field"\s*:\s*"([^"]+)"[^{}]*"value"\s*:\s*([\d.]+)[^{}]*\}', re.DOTALL)
456
  valid_fields = {'digital_presence_score','business_age_years','num_employees','monthly_cash_flow','duration','loan_rp'}
457
  for m in json_pattern.finditer(text):
@@ -459,23 +461,27 @@ def _clean_response(text: str) -> tuple:
459
  if field in valid_fields:
460
  adjustments[field] = val
461
  text = json_pattern.sub('', text).strip()
 
462
  for m in re.finditer(r'\[ADJUST:\s*(\w+)\s*=\s*([\d.]+)\]', text):
463
  field, val = m.group(1), float(m.group(2))
464
  if field not in adjustments:
465
  adjustments[field] = val
466
  text = re.sub(r'\s*\[ADJUST:[^\]]+\]\s*', ' ', text).strip()
 
467
  _standalone_action = re.compile(
468
  r',?\s*(?:coba\s+)?(?:naikkan|tingkatkan|kurangi|optimalkan|pertahankan|daftarkan|pastikan|perbaiki)'
469
  r'\s+\S+\s+(?:ke|ke\s+)[\w\s./]+(?:jt|M|juta|bulan|tahun|/bln|/month)?\.?\s*$',
470
  re.IGNORECASE
471
  )
472
  text = _standalone_action.sub('', text).strip()
 
473
  _standalone_en = re.compile(
474
  r',?\s*(?:try\s+)?(?:raise|increase|lower|reduce|optimize|improve|register)\s+'
475
  r'\S+\s+(?:to|to\s+)[\w\s./]+(?:M|jt|juta|months?|years?)?\.?\s*$',
476
  re.IGNORECASE
477
  )
478
  text = _standalone_en.sub('', text).strip()
 
479
  text = re.sub(r' +', ' ', text)
480
  text = re.sub(r' ([,.])', r'\1', text)
481
  text = re.sub(r'\n\*\*\s*\*\*\s*$', '', text, flags=re.MULTILINE).strip()
@@ -483,6 +489,7 @@ def _clean_response(text: str) -> tuple:
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,6 +591,9 @@ def _load_chat_memory():
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,19 +620,24 @@ def _summarize_history(history, lang):
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
617
 
618
  # ============================================================
619
- # SIDEBAR
 
 
 
 
 
620
  # ============================================================
621
  with st.sidebar:
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,80 +648,14 @@ with st.sidebar:
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 = {
641
- 'id': ('๐Ÿ”‡ Matikan Suara', '๐Ÿ”Š Nyalakan Suara'),
642
- 'en': ('๐Ÿ”‡ Mute Sound', '๐Ÿ”Š Unmute Sound'),
643
- 'hi': ('๐Ÿ”‡ Awaaz Band', '๐Ÿ”Š Awaaz Chalu'),
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
-
651
- _musik_active = {
652
- 'id': 'Musik aktif di tab SHAP & What-If',
653
- 'en': 'Music active on SHAP & What-If tabs',
654
- 'hi': 'Sangeet SHAP & What-If tab mein active hai',
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)'},
662
- 'dance2.gif': {'id':'Dance GIF 2 (What-If)','en':'Dance GIF 2 (What-If)','hi':'Dance GIF 2 (What-If)'},
663
- 'dance3.gif': {'id':'Dance GIF 3 (What-If)','en':'Dance GIF 3 (What-If)','hi':'Dance GIF 3 (What-If)'},
664
- 'dance4.gif': {'id':'Dance GIF 4 (What-If)','en':'Dance GIF 4 (What-If)','hi':'Dance GIF 4 (What-If)'},
665
- 'dance5.gif': {'id':'Dance GIF 5 (What-If)','en':'Dance GIF 5 (What-If)','hi':'Dance GIF 5 (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)',
680
- 'en': 'โš ๏ธ Media not uploaded (' + str(len(_missing)) + '/' + str(_total) + ' missing)',
681
- 'hi': 'โš ๏ธ Media upload nahi (' + str(len(_missing)) + '/' + str(_total) + ' missing)',
682
- }
683
- with st.expander(_warn_title.get(lang, _warn_title['id']), expanded=len(_missing) == _total):
684
- for fname, name in _missing:
685
- st.caption('โŒ ' + name)
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 'โŒ'))
707
  if not any([openrouter_key, groq_key]):
708
  st.warning("โš ๏ธ Tidak ada API key. Tambahkan OPENROUTER_API_KEY / GROQ_API_KEY di HF Secrets.")
709
 
 
710
  _llm_ph = st.empty()
711
  st.session_state['_llm_ph'] = _llm_ph
712
  if st.session_state.get('llm_status'):
@@ -716,7 +665,7 @@ with st.sidebar:
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:
@@ -737,7 +686,7 @@ with st.sidebar:
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",
@@ -803,8 +752,7 @@ _defaults = dict(
803
  wi_dig=None, wi_biz=None, wi_emp=None,
804
  wi_cash=None, wi_dur=None, wi_loan=None,
805
  chat_summary='', memory_loaded=False,
806
- gif_muted=False, llm_status='', api_log=[],
807
- _last_muted_sent=None,
808
  )
809
  for k, v in _defaults.items():
810
  if k not in st.session_state:
@@ -851,6 +799,7 @@ def shap_summary(vals, names, n=5):
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,7 +821,7 @@ def make_shap_png(shap_vals, feature_names):
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,11 +869,13 @@ def call_openrouter(messages, api_key, model=None):
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,17 +911,20 @@ 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,7 +1066,7 @@ def _get_top_issue(raw_input, pd_pct, lang):
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,7 +1074,7 @@ def _get_top_issue(raw_input, pd_pct, lang):
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,20 +1144,28 @@ CARA BERPIKIR โ€” CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
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}]
@@ -1229,26 +1191,94 @@ CARA BERPIKIR โ€” CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
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,28 +1286,113 @@ CARA BERPIKIR โ€” CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
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
 
1273
  else:
1274
  top_issue = _get_top_issue(raw_input, pd_pct, lang)
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:
@@ -1288,487 +1403,16 @@ CARA BERPIKIR โ€” CHAIN OF THOUGHT (lakukan ini secara SILENT sebelum menjawab):
1288
 
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():
1296
- def _b64(path, mime):
1297
- try:
1298
- with open(path, 'rb') as f:
1299
- data = base64.b64encode(f.read()).decode()
1300
- return "data:" + mime + ";base64," + data
1301
- except Exception:
1302
- return ""
1303
- return {
1304
- 'crying_cat': _b64('static/crying_cat.gif','image/gif'),
1305
- 'dance1': _b64('static/dance1.gif','image/gif'),
1306
- 'dance2': _b64('static/dance2.gif','image/gif'),
1307
- 'dance3': _b64('static/dance3.gif','image/gif'),
1308
- 'dance4': _b64('static/dance4.gif','image/gif'),
1309
- 'dance5': _b64('static/dance5.gif','image/gif'),
1310
- 'idk': _b64('static/idk.mp3','audio/mpeg'),
1311
- 'shake': _b64('static/shake.mp3','audio/mpeg'),
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
-
1349
- g0 = _gif("sme-gif-shap", media.get("crying_cat", ""))
1350
- g1 = _gif("sme-gif-wi1", media.get("dance1", ""))
1351
- g2 = _gif("sme-gif-wi2", media.get("dance2", ""))
1352
- g3 = _gif("sme-gif-wi3", media.get("dance3", ""))
1353
- g4 = _gif("sme-gif-wi4", media.get("dance4", ""))
1354
- g5 = _gif("sme-gif-wi5", media.get("dance5", ""))
1355
- sa = media.get("idk", "")
1356
- sw = media.get("shake", "")
1357
-
1358
- dom_str = (g0 + g1 + g2 + g3 + g4 + g5 +
1359
- '<audio id="sme-aud-shap" loop preload="auto"></audio>' +
1360
- '<audio id="sme-aud-wi" loop preload="auto"></audio>')
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!'],
@@ -1780,13 +1424,6 @@ _tab_load_steps = {
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
  # ============================================================
@@ -1801,64 +1438,54 @@ t1, t2, t3, t4, t5 = st.tabs([
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
  )
@@ -1868,85 +1495,86 @@ 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")
@@ -1985,11 +1613,11 @@ with t4:
1985
  'monthly_cash_flow': float(wi_cash * 1e6),
1986
  'num_employees': wi_emp,
1987
  }
1988
- X_wi = preprocess(wi_raw, scaler, feature_names)
1989
- wi_pd = float(ensemble.predict_proba(X_wi)[0][1])
1990
  wi_res = risk_result(wi_pd, wi_loan * 1e6, result_f['lgd'])
1991
- d_pd = wi_pd - result_f['pd']
1992
- d_el = wi_res['el'] - result_f['el']
1993
  _log_api("What-If", "Local Model", True, 0)
1994
 
1995
  r1, r2, r3 = st.columns(3)
@@ -2051,15 +1679,25 @@ with t4:
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
 
@@ -2072,70 +1710,53 @@ with t4:
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
- )
 
1
  # ============================================================
2
+ # ๐Ÿฆ SME Credit Risk Assessment โ€” FINAL
3
  # Final Project | AI Engineering Bootcamp Batch 10
4
  # Author: 1na37
5
  # ============================================================
6
+ # Basis: OLDER untuk Maya AI logic, CoT, persona
7
+ # TRANSLATION untuk Hindi Latin transliteration
8
+ # NEWER untuk bug fixes: API Tracker
9
+ # FIX: _set_status ternary โ†’ if/else (Python 3.13 compat)
10
+ # FIX: Sidebar reorder sesuai permintaan
 
 
11
  # ============================================================
12
 
13
  import streamlit as st
 
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
  '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
  '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
  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
  LANG_LABELS = {'id':'๐Ÿ‡ฎ๐Ÿ‡ฉ Bahasa Indonesia','en':'๐Ÿ‡ฌ๐Ÿ‡ง English','hi':'๐Ÿ‡ฎ๐Ÿ‡ณ Hindi (Roman)'}
221
 
222
  # ============================================================
223
+ # CSS STYLING
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
299
  # ============================================================
300
  def _set_status(msg: str):
301
  st.session_state['llm_status'] = msg
 
313
  pass
314
 
315
  # ============================================================
316
+ # FORMAL TOOL CALLING SCHEMA (Agentic AI)
317
  # ============================================================
318
  MAYA_TOOLS = [
319
  {
 
342
  }
343
  ]
344
 
345
+ # Tool-capable model subsets
346
  _OR_TOOL_MODELS = [
347
  "google/gemini-2.0-flash-exp:free",
348
  "qwen/qwen3-32b:free",
 
357
  ]
358
 
359
  # ============================================================
360
+ # LLM API CALLERS (with status + timing)
361
  # ============================================================
362
  def call_openrouter_tools(messages, api_key, tools=None):
363
  if not api_key:
 
453
  return text, {}
454
  adjustments = {}
455
  text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
456
+
457
  json_pattern = re.compile(r'\{[^{}]*"field"\s*:\s*"([^"]+)"[^{}]*"value"\s*:\s*([\d.]+)[^{}]*\}', re.DOTALL)
458
  valid_fields = {'digital_presence_score','business_age_years','num_employees','monthly_cash_flow','duration','loan_rp'}
459
  for m in json_pattern.finditer(text):
 
461
  if field in valid_fields:
462
  adjustments[field] = val
463
  text = json_pattern.sub('', text).strip()
464
+
465
  for m in re.finditer(r'\[ADJUST:\s*(\w+)\s*=\s*([\d.]+)\]', text):
466
  field, val = m.group(1), float(m.group(2))
467
  if field not in adjustments:
468
  adjustments[field] = val
469
  text = re.sub(r'\s*\[ADJUST:[^\]]+\]\s*', ' ', text).strip()
470
+
471
  _standalone_action = re.compile(
472
  r',?\s*(?:coba\s+)?(?:naikkan|tingkatkan|kurangi|optimalkan|pertahankan|daftarkan|pastikan|perbaiki)'
473
  r'\s+\S+\s+(?:ke|ke\s+)[\w\s./]+(?:jt|M|juta|bulan|tahun|/bln|/month)?\.?\s*$',
474
  re.IGNORECASE
475
  )
476
  text = _standalone_action.sub('', text).strip()
477
+
478
  _standalone_en = re.compile(
479
  r',?\s*(?:try\s+)?(?:raise|increase|lower|reduce|optimize|improve|register)\s+'
480
  r'\S+\s+(?:to|to\s+)[\w\s./]+(?:M|jt|juta|months?|years?)?\.?\s*$',
481
  re.IGNORECASE
482
  )
483
  text = _standalone_en.sub('', text).strip()
484
+
485
  text = re.sub(r' +', ' ', text)
486
  text = re.sub(r' ([,.])', r'\1', text)
487
  text = re.sub(r'\n\*\*\s*\*\*\s*$', '', text, flags=re.MULTILINE).strip()
 
489
  return text, adjustments
490
 
491
  def _call_chat_llm(messages):
492
+ """Cascade: OR tools โ†’ Groq tools โ†’ OR no-tools โ†’ Groq no-tools."""
493
  _or = st.session_state.get("openrouter_key", "")
494
  _grq = st.session_state.get("groq_key", "")
495
  if _or:
 
591
  pass
592
  return [], ""
593
 
594
+ # ============================================================
595
+ # MEMORY SUMMARIZATION
596
+ # ============================================================
597
  def _summarize_history(history, lang):
598
  if len(history) <= 8:
599
  return history, st.session_state.get('chat_summary', '')
 
620
  st.session_state.chat_summary = new_summary
621
  return recent_turns, new_summary
622
 
623
+ # Early init
624
+ for _k, _v in [('llm_status', ''), ('api_log', [])]:
625
  if _k not in st.session_state:
626
  st.session_state[_k] = _v
627
 
628
  # ============================================================
629
+ # SIDEBAR โ€” reordered:
630
+ # 1. Title
631
+ # 2. Language
632
+ # 3. API key status + live LLM status
633
+ # 4. API Tracker
634
+ # 5. Stack / footer
635
  # ============================================================
636
  with st.sidebar:
637
  st.markdown("## ๐Ÿฆ SME Credit Risk AI")
638
  st.markdown("---")
639
 
640
+ # 1. Language selector
641
  if 'lang_sel' not in st.session_state:
642
  st.session_state.lang_sel = 'id'
643
  lang = st.radio(
 
648
  )
649
  st.markdown("---")
650
 
651
+ # 2. API key status + live LLM status indicator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
  openrouter_key = st.session_state.openrouter_key
653
  groq_key = st.session_state.groq_key
654
  st.caption("๐Ÿ”‘ **OpenRouter:** " + ('โœ…' if openrouter_key else 'โŒ') + " **Groq:** " + ('โœ…' if groq_key else 'โŒ'))
655
  if not any([openrouter_key, groq_key]):
656
  st.warning("โš ๏ธ Tidak ada API key. Tambahkan OPENROUTER_API_KEY / GROQ_API_KEY di HF Secrets.")
657
 
658
+ # Live status placeholder โ€” updated by _set_status() during LLM calls
659
  _llm_ph = st.empty()
660
  st.session_state['_llm_ph'] = _llm_ph
661
  if st.session_state.get('llm_status'):
 
665
  pass
666
  st.markdown("---")
667
 
668
+ # 3. API Tracker
669
  st.markdown("### ๐Ÿ“ก API Tracker")
670
  _active_status = st.session_state.get('llm_status', '')
671
  if _active_status:
 
686
  st.rerun()
687
  st.markdown("---")
688
 
689
+ # 4. Stack + footer
690
  st.markdown(T('sidebar_stack', lang))
691
  stack_items = {
692
  '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",
 
752
  wi_dig=None, wi_biz=None, wi_emp=None,
753
  wi_cash=None, wi_dur=None, wi_loan=None,
754
  chat_summary='', memory_loaded=False,
755
+ llm_status='', api_log=[],
 
756
  )
757
  for k, v in _defaults.items():
758
  if k not in st.session_state:
 
799
  ])
800
 
801
  def make_shap_png(shap_vals, feature_names):
802
+ """Returns base64 data URI string."""
803
  sv = np.array(shap_vals)
804
  names = list(feature_names)
805
  n = min(12, len(sv))
 
821
  return 'data:image/png;base64,' + b64
822
 
823
  # ============================================================
824
+ # LLM HELPERS โ€” Narrative cascade
825
  # ============================================================
826
  _OR_FREE_MODELS = [
827
  "google/gemini-2.0-flash-exp:free",
 
869
  except requests.exceptions.Timeout:
870
  ms = int((_time.time() - t0) * 1000)
871
  last_error = "Timeout (" + m + ")"
872
+ short = m.split('/')[-1].replace(':free','')
873
+ _log_api("Narasi", "OR/" + short, False, ms)
874
  except Exception as e:
875
  ms = int((_time.time() - t0) * 1000)
876
  last_error = m + ": " + str(e)[:100]
877
+ short = m.split('/')[-1].replace(':free','')
878
+ _log_api("Narasi", "OR/" + short, False, ms)
879
  return None, last_error
880
 
881
  def call_groq(messages, api_key):
 
911
  return None, last_error
912
 
913
  def _call_llm(messages):
914
+ """Narrative LLM: OpenRouter free โ†’ Groq free โ†’ None."""
915
  _or = st.session_state.get("openrouter_key", "")
916
  _grq = st.session_state.get("groq_key", "")
917
  last_err = None
918
  if _or:
919
  r, last_err = call_openrouter(messages, _or)
920
  if r:
921
+ model_used = _OR_FREE_MODELS[0].split("/")[-1].replace(":free","")
922
+ return r, 'OpenRouter (' + model_used + ')', None
923
  if _grq:
924
  r, last_err = call_groq(messages, _grq)
925
  if r:
926
+ model_used = _GROQ_FREE_MODELS[0]
927
+ return r, 'Groq (' + model_used + ')', None
928
  return None, None, last_err or "No API key configured"
929
 
930
  # ============================================================
 
1066
  if not npwp:
1067
  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")
1068
  elif dig < 40:
1069
+ 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")
1070
  elif cf < 10e6:
1071
  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")
1072
  elif biz < 2:
 
1074
  elif pd_pct < 20:
1075
  return _t("profil kamu sudah sangat bagus!","your profile is already excellent!","aapka parichay pehle se behtareen hai!")
1076
  else:
1077
+ 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!")
1078
 
1079
  # ============================================================
1080
  # AI CHAT โ€” Maya persona
 
1144
  "- Pinjaman: Rp " + str(int(raw_input.get('loan_rp',50e6)/1e6)) + "M | Tenor: " + str(raw_input.get('duration',24)) + " bulan\n"
1145
  "- Riwayat Kredit: " + str(raw_input.get('credit_history')) + "\n\n"
1146
  "FAKTOR RISIKO UTAMA (SHAP):\n" + factors + "\n"
1147
+ + context
1148
+ + summary_block
1149
+ + cot_block
1150
+ + fewshot
1151
+ + "ATURAN MENJAWAB โ€” BACA SEMUA DENGAN SEKSAMA:\n"
1152
+ "1. Jawab BEBAS โ€” tidak harus soal kredit. Kalau ditanya soal diri sendiri, perkenalkan sebagai Maya.\n"
1153
  "2. Kalau relevan, selalu hubungkan ke data pemohon dengan menyebut angka aktualnya.\n"
1154
  "3. Berikan saran KONKRET dan SPESIFIK, bukan generik.\n"
1155
  "4. Maksimal 150 kata kecuali diminta lebih panjang.\n"
1156
+ "5. FORMAT TAG [ADJUST:] โ€” WAJIB IKUTI ATURAN INI:\n"
1157
+ " BENAR: Embed tag langsung di akhir kalimat saran:\n"
1158
+ " 'Coba naikkan digital score ke 75 [ADJUST: digital_presence_score=75] โ€” dampaknya paling besar.'\n"
1159
+ " SALAH: Menulis kalimat saran TANPA tag, lalu menambah baris terpisah\n"
1160
+ " SALAH: Menulis tag di baris sendiri di akhir response\n"
1161
+ " SALAH BESAR: Menulis ulang rekomendasi di baris terpisah setelah selesai\n"
1162
+ " RULE: Setiap rekomendasi bernilai angka = SATU kalimat + SATU tag [ADJUST:] inline. TIDAK LEBIH.\n"
1163
+ " RULE: JANGAN pernah tulis JSON object {...} di response.\n"
1164
+ " RULE: Field valid: digital_presence_score(1-100), business_age_years(1-20),\n"
1165
  " num_employees(1-50), monthly_cash_flow(angka Rp), duration(4-72), loan_rp(angka Rp)\n"
1166
  "6. JANGAN pernah balik ke template. Jawab seperti manusia cerdas.\n"
1167
  "7. Baca context percakapan sebelumnya sebelum menjawab โ€” jaga konsistensi topik.\n"
1168
+ "8. AKHIRI response dengan kalimat lengkap. Jangan tambahkan apapun setelah kalimat terakhir.\n"
1169
  )
1170
 
1171
  messages = [{"role": "system", "content": system}]
 
1191
 
1192
  if any(w in low for w in ['siapa','kamu','km','who are you','nama','you are','maya','halo','hai','hei','hello','hi','perkenalan']):
1193
  response = _t(
1194
+ "Hei! Aku **Maya**, AI Credit Advisor kamu Aku dirancang untuk bantu kamu pahami skor kredit dan strategi bisnis UMKM. "
1195
+ "Skor PD kamu sekarang **" + str(round(pd_pct,1)) + "%** โ€” " + ('sudah bagus banget!' if pd_pct < 20 else 'masih ada ruang untuk diperbaiki.') + " "
1196
+ "Mau aku jelasin lebih detail atau ada yang mau ditanyain?",
1197
+ "Hey! I'm **Maya**, your AI Credit Advisor. I help you understand your credit score and SME business strategy. "
1198
+ "Your PD score is **" + str(round(pd_pct,1)) + "%** โ€” " + ('looking great!' if pd_pct < 20 else 'there is room to improve.') + " "
1199
+ "Anything you want to ask?",
1200
+ "Namaste! Main **Maya** hoon, aapki AI Credit Advisor. "
1201
+ "Aapka PD score **" + str(round(pd_pct,1)) + "%** hai โ€” " + ('bahut badhiya!' if pd_pct < 20 else 'sudhaar ki gunjaish hai.') + " "
1202
+ "Kuch poochna hai?"
1203
+ )
1204
+
1205
+ elif any(w in low for w in ['cuan','profit','untung','laba','cepet cuan','penghasilan cepat','cara cepet','duit cepet']):
1206
+ response = _t(
1207
+ "Cuan cepet? Sah-sah aja! Tapi di kredit, yang bikin bank percaya bukan seberapa cepat, tapi **konsistensi**. "
1208
+ "Dari profil kamu (cash flow Rp " + str(int(cf/1e6)) + "jt/bln, digital " + str(dig) + "/100):\n\n"
1209
+ "1. **Aktifin marketplace** โ†’ cash flow naik [ADJUST: monthly_cash_flow=25000000]\n"
1210
+ "2. **Google Business aktif** โ†’ digital score naik [ADJUST: digital_presence_score=75]\n"
1211
+ "3. **Dokumentasikan semua pemasukan** โ€” pisah rekening bisnis & pribadi",
1212
+ "Quick profit? Totally valid! But in credit, **consistency** matters more. "
1213
+ "From your profile (CF Rp " + str(int(cf/1e6)) + "M/mo, digital " + str(dig) + "/100):\n\n"
1214
+ "1. Active marketplace โ†’ CF up [ADJUST: monthly_cash_flow=25000000]\n"
1215
+ "2. Google Business โ†’ digital up [ADJUST: digital_presence_score=75]\n"
1216
+ "3. Document all income streams",
1217
+ "Jaldi munafa? **Niyamitata** zaroori hai.\n"
1218
+ "1) Marketplace active karein [ADJUST: monthly_cash_flow=25000000] "
1219
+ "2) Google Business [ADJUST: digital_presence_score=75] 3) Aay document karein"
1220
  )
1221
+ adjustments['digital_presence_score'] = 75
1222
+ adjustments['monthly_cash_flow'] = 25000000
1223
+
1224
+ elif any(w in low for w in ['improve','better','lower','reduce','tingkatkan','kurangi','turunkan','cara','gimana','bagaimana','naik','turun','meningkat','naikkan','optimalkan']):
1225
  tips = []
1226
  if not npwp:
1227
+ tips.append(_t("1. **Urus NPWP** โ€” wajib untuk pinjaman formal, daftar di pajak.go.id",
1228
+ "1. **Register NPWP** โ€” required for formal loans, apply at pajak.go.id",
1229
+ "1. **NPWP register karein** โ€” aupchaarik rin ke liye zaroori"))
1230
+ adjustments['has_npwp'] = 1
1231
  if dig < 75:
1232
  n = '2' if not npwp else '1'
1233
+ tips.append(_t(n + ". **Naikkan Digital Score dari " + str(dig) + " ke 75+** โ€” aktif di Google Business, Tokopedia/Shopee",
1234
+ n + ". **Raise Digital Score from " + str(dig) + " to 75+** โ€” Google Business, marketplace",
1235
+ n + ". **Digital Score " + str(dig) + " se 75+ karein** โ€” Google Business, marketplace"))
1236
  adjustments['digital_presence_score'] = 75
1237
  if cf < 25e6:
1238
  n = str(len(tips) + 1)
1239
+ tips.append(_t(n + ". **Optimalkan cash flow dari Rp " + str(int(cf/1e6)) + "jt ke 25jt+/bln** โ€” diversifikasi produk",
1240
+ n + ". **Grow cash flow from Rp " + str(int(cf/1e6)) + "M to 25M+/month**",
1241
+ n + ". **Naqad pravaah Rp " + str(int(cf/1e6)) + "M se 25M+ karein**"))
1242
  adjustments['monthly_cash_flow'] = 25000000
1243
  if not tips:
1244
+ tips.append(_t("Profil kamu sudah solid dengan PD " + str(round(pd_pct,1)) + "%! Coba simulasikan di tab What-If.",
1245
+ "Your profile is solid at PD " + str(round(pd_pct,1)) + "%! Try the What-If tab.",
1246
+ "Aapka parichay PD " + str(round(pd_pct,1)) + "% ke saath mazboot hai!"))
1247
+ response = (
1248
+ _t("Untuk turunkan PD dari **" + str(round(pd_pct,1)) + "%**, fokus ke:\n\n",
1249
+ "To lower PD from **" + str(round(pd_pct,1)) + "%**, focus on:\n\n",
1250
+ "PD **" + str(round(pd_pct,1)) + "%** kam karne ke liye:\n\n")
1251
+ + '\n'.join(tips[:3])
1252
+ )
1253
+
1254
+ elif any(w in low for w in ['why','kenapa','mengapa','factor','faktor','shap','pengaruh','driver','apa yang','jelasin','jelaskan','explain','definisi']):
1255
+ ti = int(np.argsort(np.abs(shap_vals))[-1])
1256
+ ti2 = int(np.argsort(np.abs(shap_vals))[-2])
1257
+ fn = feature_names[ti]
1258
+ if 'business_age' in fn:
1259
+ analogi = _t(" โ€” ibarat pengalaman kerja, makin lama makin dipercaya"," โ€” like work experience, longer = more trustworthy"," โ€” kaam ke anubhav ki tarah, zyada = zyada bharosemand")
1260
+ elif 'cash_flow' in fn:
1261
+ 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")
1262
+ elif 'digital' in fn:
1263
+ 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'")
1264
+ else:
1265
+ analogi = ""
1266
+ response = _t(
1267
+ "Dua faktor terbesar yang drive skor PD **" + str(round(pd_pct,1)) + "%** kamu:\n\n"
1268
+ "1. **" + fn + "**" + analogi + "\n โ†’ " + ('meningkatkan' if shap_vals[ti] > 0 else 'menurunkan') + " risiko (SHAP=" + str(round(shap_vals[ti],3)) + ")\n\n"
1269
+ "2. **" + feature_names[ti2] + "**\n โ†’ " + ('meningkatkan' if shap_vals[ti2] > 0 else 'menurunkan') + " risiko (SHAP=" + str(round(shap_vals[ti2],3)) + ")\n\n"
1270
+ + ('Skor bagus! Kedua faktor ini justru mendukung kelayakan kamu.' if pd_pct < 20 else 'Fokus perbaiki faktor pertama dulu โ€” dampaknya paling besar.'),
1271
+ "Two biggest drivers of your PD **" + str(round(pd_pct,1)) + "%**:\n\n"
1272
+ "1. **" + fn + "**" + analogi + "\n โ†’ " + ('increases' if shap_vals[ti] > 0 else 'decreases') + " risk (SHAP=" + str(round(shap_vals[ti],3)) + ")\n\n"
1273
+ "2. **" + feature_names[ti2] + "**\n โ†’ " + ('increases' if shap_vals[ti2] > 0 else 'decreases') + " risk (SHAP=" + str(round(shap_vals[ti2],3)) + ")\n\n"
1274
+ + ('Great score! Both factors support your eligibility.' if pd_pct < 20 else 'Focus on the first factor โ€” it has the biggest impact.'),
1275
+ "Aapke PD **" + str(round(pd_pct,1)) + "%** ke do mukhya kaarak:\n\n"
1276
+ "1. **" + fn + "**" + analogi + "\n โ†’ " + ('badhata' if shap_vals[ti] > 0 else 'ghataata') + " jokhim (SHAP=" + str(round(shap_vals[ti],3)) + ")\n\n"
1277
+ "2. **" + feature_names[ti2] + "**\n โ†’ " + ('badhata' if shap_vals[ti2] > 0 else 'ghataata') + " jokhim (SHAP=" + str(round(shap_vals[ti2],3)) + ")\n\n"
1278
+ + ('Badhiya score! Dono kaarak yogyata ka samarthan karte hain.' if pd_pct < 20 else 'Pehle pehle kaarak sudhaarein โ€” sabse bada prabhav.')
1279
+ )
1280
+
1281
+ elif any(w in low for w in ['pinjaman','loan','kredit','berapa','amount','besar','limit','ideal','rekomendasi pinjaman']):
1282
  if cf > 0:
1283
  dur_val = raw_input.get('duration', 24)
1284
  safe = cf * dur_val * 0.35
 
1286
  if cur > safe:
1287
  adjustments['loan_rp'] = safe
1288
  response = _t(
1289
+ "Berdasarkan cash flow kamu **Rp " + str(int(cf/1e6)) + "jt/bulan** dan tenor " + str(int(dur_val)) + " bulan, "
1290
+ "pinjaman ideal maksimal **Rp " + str(int(safe/1e6)) + "jt** (rasio cicilan 35%).\n\n"
1291
+ "Pinjaman kamu sekarang Rp " + str(int(cur/1e6)) + "jt โ€” "
1292
+ + ('masih aman, good job!' if cur <= safe else 'melebihi batas aman. Pertimbangkan kurangi ke Rp ' + str(int(safe/1e6)) + 'jt atau perpanjang tenor.'),
1293
+ "Based on your cash flow **Rp " + str(int(cf/1e6)) + "M/month** and " + str(int(dur_val)) + "-month tenure, "
1294
+ "ideal loan is max **Rp " + str(int(safe/1e6)) + "M** (35% installment ratio).\n\n"
1295
+ "Current loan Rp " + str(int(cur/1e6)) + "M โ€” "
1296
+ + ('within safe limits, good job!' if cur <= safe else 'exceeds safe limit. Consider reducing to Rp ' + str(int(safe/1e6)) + 'M.'),
1297
+ "Aapke naqad pravaah **Rp " + str(int(cf/1e6)) + "M/maah** aur " + str(int(dur_val)) + " maah ke aadhaar par "
1298
+ "adhiktam rin **Rp " + str(int(safe/1e6)) + "M** (35% kist anupaat).\n\n"
1299
+ "Vartamaan rin Rp " + str(int(cur/1e6)) + "M โ€” " + ('surakshit!' if cur <= safe else 'seema se adhik.')
1300
  )
1301
  else:
1302
+ response = _t("Isi cash flow bulanan di form dulu ya, biar aku bisa kasih rekomendasi akurat!",
1303
+ "Fill in your monthly cash flow first for an accurate recommendation!",
1304
+ "Sahi sujhaav ke liye pehle maasik naqad pravaah bharein!")
1305
+
1306
+ elif any(w in low for w in ['tabungan','saving','nabung','menabung','savings','uang','keuangan','finance','financial','simpan']):
1307
  response = _t(
1308
+ "Soal tabungan dan cash flow kamu (saat ini **Rp " + str(int(cf/1e6)) + "jt/bln**):\n\n"
1309
+ "3 cara praktis tingkatkan tabungan UMKM:\n"
1310
+ "1. Pisahkan rekening bisnis & pribadi โ€” biar tidak tercampur\n"
1311
+ "2. Sisihkan minimal 10-15% dari omzet setiap bulan secara otomatis\n"
1312
+ "3. Dokumentasikan semua pemasukan โ€” ini juga naikkan digital score!\n\n"
1313
+ "Tabungan naik โ†’ cash flow lebih sehat โ†’ PD turun",
1314
+ "About savings and your cash flow (currently **Rp " + str(int(cf/1e6)) + "M/month**):\n\n"
1315
+ "3 practical SME savings tips:\n"
1316
+ "1. Separate business & personal bank accounts\n"
1317
+ "2. Auto-transfer 10-15% of monthly revenue to savings\n"
1318
+ "3. Document all income streams โ€” this also boosts digital score!\n\n"
1319
+ "More savings โ†’ healthier cash flow โ†’ lower PD",
1320
+ "Bachat aur naqad pravaah (**Rp " + str(int(cf/1e6)) + "M/maah**) ke baare mein:\n\n"
1321
+ "3 vyaavhaarik tarike:\n"
1322
+ "1. Vyapaar aur vyaktigat khaate alag rakhein\n"
1323
+ "2. Maasik aay ka 10-15% bachaen\n"
1324
+ "3. Sabhi aay document karein โ€” digital score bhi badhega!\n\n"
1325
+ "Zyada bachat โ†’ swasth naqad pravaah โ†’ PD kam"
1326
+ )
1327
+ adjustments['monthly_cash_flow'] = int(cf * 1.3)
1328
+
1329
+ elif any(w in low for w in ['riwayat kredit','credit history','kredit history','credit record','riwayat','history kredit']):
1330
+ ch_val = raw_input.get('credit_history', '-')
1331
+ response = _t(
1332
+ "**Riwayat kredit** = catatan sejarah bayar utangmu โ€” ibarat rapor keuangan.\n\n"
1333
+ "Status kamu: **" + ch_val + "**\n\n"
1334
+ "Dampak ke skor kredit:\n"
1335
+ "- All paid / Existing paid โ†’ sinyal positif\n"
1336
+ "- Delayed previously โ†’ bank waspada\n"
1337
+ "- Critical โ†’ risiko naik signifikan\n\n"
1338
+ "Cara bangun riwayat bagus: bayar cicilan tepat waktu, jangan ambil pinjaman melebihi kemampuan bayar.",
1339
+ "**Credit history** = your track record of paying debts โ€” your financial report card.\n\n"
1340
+ "Your status: **" + ch_val + "**\n\n"
1341
+ "Score impact:\n"
1342
+ "- All paid / Existing paid โ†’ positive signal\n"
1343
+ "- Delayed previously โ†’ bank is cautious\n"
1344
+ "- Critical โ†’ significant risk increase\n\n"
1345
+ "Build good history: pay on time, don't borrow beyond your repayment capacity.",
1346
+ "**Credit itihaas** = karz bhugtaan ka record โ€” vitteey report card.\n\n"
1347
+ "Aapki sthiti: **" + ch_val + "**\n\n"
1348
+ "Score prabhav:\n"
1349
+ "- All paid / Existing paid โ†’ sakaraatmak sanket\n"
1350
+ "- Delayed previously โ†’ bank saavdhan\n"
1351
+ "- Critical โ†’ jokhim zyada\n\n"
1352
+ "Samay par bhugtaan karein, kshamta se adhik rin na lein."
1353
+ )
1354
+
1355
+ elif any(w in low for w in ['tips','bisnis','usaha','umkm','sme','digital','marketplace','online','ecommerce','strategi','business tips']):
1356
+ response = _t(
1357
+ "Tips bisnis UMKM buat kamu (PD " + str(round(pd_pct,1)) + "%):\n\n"
1358
+ "1. **Digital hadir** โ€” daftar Google Business Profile, aktif di Tokopedia/Shopee/TikTok Shop\n"
1359
+ " (Digital score kamu " + str(dig) + "/100 โ€” masih bisa naik!)\n"
1360
+ "2. **Pisah keuangan** โ€” rekening bisnis terpisah dari pribadi\n"
1361
+ "3. **Dokumentasi rutin** โ€” catat semua transaksi, ini bukti ke bank\n"
1362
+ "4. **Legalitas** โ€” NPWP & SIUP/NIB buka akses ke KUR & kredit formal\n\n"
1363
+ "Mau aku simulasikan dampaknya ke skor di tab What-If?",
1364
+ "SME business tips for you (PD " + str(round(pd_pct,1)) + "%):\n\n"
1365
+ "1. **Go digital** โ€” Google Business Profile, Tokopedia/Shopee/TikTok Shop\n"
1366
+ " (Your digital score " + str(dig) + "/100 โ€” room to improve!)\n"
1367
+ "2. **Separate finances** โ€” dedicated business bank account\n"
1368
+ "3. **Document everything** โ€” track all transactions as proof for banks\n"
1369
+ "4. **Get legal** โ€” NPWP & SIUP/NIB unlock KUR & formal credit\n\n"
1370
+ "Want me to simulate the impact in the What-If tab?",
1371
+ "SME vyapaar tips (PD " + str(round(pd_pct,1)) + "%):\n\n"
1372
+ "1. **Digital upasthiti** โ€” Google Business Profile, Tokopedia/Shopee\n"
1373
+ " (Aapka digital score " + str(dig) + "/100 โ€” sudhaar ki gunjaish!)\n"
1374
+ "2. **Alag vitteey khaata** โ€” vyapaar ka alag bank khaata\n"
1375
+ "3. **Sab document karein** โ€” sabhi laanden-denden ka record\n"
1376
+ "4. **Kanooni rahein** โ€” NPWP & SIUP/NIB KUR unlock karta hai\n\n"
1377
+ "Kya main What-If tab mein prabhav simulate karun?"
1378
  )
1379
  if dig < 75:
1380
  adjustments['digital_presence_score'] = 75
1381
+
1382
  else:
1383
  top_issue = _get_top_issue(raw_input, pd_pct, lang)
1384
  has_key = any([st.session_state.get("openrouter_key"), st.session_state.get("groq_key")])
1385
  err_note = "\n\nLLM error: " + str(_last_error)[:80] if (has_key and _last_error) else ""
1386
  response = _t(
1387
+ "Pertanyaan menarik! Untuk ini aku butuh koneksi AI.\n\n"
1388
+ "Yang bisa aku kasih tahu: PD kamu **" + str(round(pd_pct,1)) + "%** dan " + top_issue + "\n\n"
1389
+ "Coba tanya yang lebih spesifik: cara turunkan skor, faktor risiko, pinjaman ideal, tips tabungan, atau riwayat kredit." + err_note,
1390
+ "Great question! For this I need an AI connection.\n\n"
1391
+ "What I can share: your PD is **" + str(round(pd_pct,1)) + "%** and " + top_issue + "\n\n"
1392
+ "Try asking specifically: how to lower score, risk factors, ideal loan, savings tips, or credit history." + err_note,
1393
+ "Accha sawaal! Iske liye AI connection chahiye.\n\n"
1394
+ "Aapka PD **" + str(round(pd_pct,1)) + "%** hai aur " + top_issue + "\n\n"
1395
+ "Vishesh roop se puchein: score kaise kam karein, jokhim kaarak, ideal rin, bachat tips." + err_note
1396
  )
1397
 
1398
  if response:
 
1403
 
1404
  return response or "...", adjustments, _last_error
1405
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1406
  # ============================================================
1407
  # TAB LOADING PROGRESS
1408
  # ============================================================
1409
+ def _tab_step(n):
1410
+ try:
1411
+ pct = int(n / 5 * 100)
1412
+ _tab_prog_ph.progress(pct, text=_tls[n])
1413
+ except Exception:
1414
+ pass
1415
+
1416
  _tab_load_steps = {
1417
  'id': ['๐Ÿ” Menyiapkan SHAP...', '๐Ÿ’ฌ Menyiapkan Narasi AI...', '๐Ÿค– Menyiapkan Chat...',
1418
  '๐ŸŽฎ Menyiapkan What-If...', '๐Ÿ“ Menyiapkan Formula...', 'โœ… Semua tab siap!'],
 
1424
  _tls = _tab_load_steps.get(lang, _tab_load_steps['id'])
1425
  _tab_prog_ph = st.empty()
1426
 
 
 
 
 
 
 
 
1427
  _tab_step(0)
1428
 
1429
  # ============================================================
 
1438
  with t1:
1439
  _tab_step(0)
1440
  st.markdown('<div class="sec-title">' + T("shap_title",lang) + '</div>', unsafe_allow_html=True)
1441
+ _shap_all_zero = (shap_vals is not None and np.all(shap_vals == 0))
1442
+ if _shap_all_zero:
1443
+ st.warning(
1444
+ "โš ๏ธ **SHAP tidak tersedia** โ€” model XGBoost (`xgb_model.pkl`) mungkin belum di-upload atau "
1445
+ "tidak kompatibel dengan SHAP TreeExplainer. "
1446
+ "Pastikan `model/xgb_model.pkl` ada di HF Space dan di-train ulang jika perlu.\n\n"
1447
+ "Skor PD dan narasi tetap valid (menggunakan ensemble model)."
1448
+ )
1449
  else:
1450
+ if st.session_state.get("shap_png"):
1451
+ _spng = st.session_state.shap_png
1452
+ if isinstance(_spng, str) and _spng.startswith('data:'):
1453
+ st.markdown('<img src="' + _spng + '" style="width:100%;border-radius:8px;">', unsafe_allow_html=True)
1454
+ elif _spng:
1455
+ st.image(_spng, use_container_width=True)
1456
+ st.caption(T('shap_caption', lang))
1457
+ top_idx = np.argsort(np.abs(shap_vals))[-10:][::-1]
1458
+ st.dataframe(pd.DataFrame({
1459
+ T('shap_col_feature',lang): [feature_names[i] for i in top_idx],
1460
+ 'SHAP': [round(shap_vals[i],4) for i in top_idx],
1461
+ T('shap_col_impact',lang): [
1462
+ T('shap_risk_up',lang) if shap_vals[i] > 0 else T('shap_risk_down',lang)
1463
+ for i in top_idx
1464
+ ],
1465
+ }), use_container_width=True, hide_index=True)
 
 
 
 
 
 
 
1466
 
1467
  # โ”€โ”€ TAB 2: NARRATIVE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1468
  with t2:
1469
  _tab_step(1)
1470
  flag = {'id':'๐Ÿ‡ฎ๐Ÿ‡ฉ','en':'๐Ÿ‡ฌ๐Ÿ‡ง','hi':'๐Ÿ‡ฎ๐Ÿ‡ณ'}[lang]
1471
  st.markdown('<div class="sec-title">' + T("narr_title",lang) + ' ' + flag + '</div>', unsafe_allow_html=True)
1472
+ st.caption(T('narr_source',lang) + ": " + (llm_src if llm_src else "โ€”"))
1473
+ if narrative and narrative.strip():
1474
+ fmt = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', narrative.replace('\n','<br>'))
1475
+ st.markdown('<div class="narrative-box">' + fmt + '</div>', unsafe_allow_html=True)
 
1476
  else:
1477
+ narrative_fb, src_fb = get_narrative(shap_vals, feature_names, result, lang, raw_input)
1478
+ st.session_state.narrative = narrative_fb
1479
+ st.session_state.llm_src = src_fb
1480
+ fmt = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', narrative_fb.replace('\n','<br>'))
1481
+ st.markdown('<div class="narrative-box">' + fmt + '</div>', unsafe_allow_html=True)
 
 
 
 
 
1482
  _or_key = st.session_state.get("openrouter_key","")
1483
  _grq_key = st.session_state.get("groq_key","")
1484
+ with st.expander("LLM Debug Info"):
1485
  st.code(
1486
  "OR key : " + ('OK ' + _or_key[:12] + '...' if _or_key else 'missing') + "\n"
1487
  "Groq key : " + ('OK ' + _grq_key[:12] + '...' if _grq_key else 'missing') + "\n"
1488
+ "Source : " + str(src_fb) + "\n"
1489
  "OR model : " + _OR_FREE_MODELS[0] + "\n"
1490
  "Groq mdl : " + _GROQ_FREE_MODELS[0]
1491
  )
 
1495
  _tab_step(2)
1496
  st.markdown('<div class="sec-title">' + T("chat_title",lang) + '</div>', unsafe_allow_html=True)
1497
 
1498
+ if st.session_state.get('chat_summary'):
1499
+ n_turns = len(st.session_state.chat_history)
1500
+ lbl = {
1501
+ 'id': 'Memory aktif - ' + str(n_turns) + ' pesan tersimpan',
1502
+ 'en': 'Memory active - ' + str(n_turns) + ' messages saved',
1503
+ 'hi': 'Memory active - ' + str(n_turns) + ' sandesh save hue',
1504
+ }
1505
+ st.markdown('<div class="memory-badge">' + lbl.get(lang, lbl["en"]) + '</div>', unsafe_allow_html=True)
1506
+
1507
+ chips = [T('chat_chip1',lang), T('chat_chip2',lang), T('chat_chip3',lang), T('chat_chip4',lang)]
1508
+ chip_cols = st.columns(len(chips))
1509
+ chip_clicked = None
1510
+ for i, (col, chip) in enumerate(zip(chip_cols, chips)):
1511
+ with col:
1512
+ if st.button(chip, key="chip_" + str(i), use_container_width=True):
1513
+ chip_clicked = chip
1514
+ st.markdown("---")
1515
+
1516
+ if st.session_state.chat_history:
1517
+ html_chat = ""
1518
+ for msg in st.session_state.chat_history:
1519
+ content = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>',
1520
+ msg['content'].replace('\n','<br>'))
1521
+ cls = "chat-bubble-user" if msg['role'] == 'user' else "chat-bubble-ai"
1522
+ icon = "๐Ÿ‘ค" if msg['role'] == 'user' else "๐Ÿค–"
1523
+ html_chat += '<div class="' + cls + '">' + icon + ' ' + content + '</div>'
1524
+ st.markdown(html_chat, unsafe_allow_html=True)
1525
  else:
1526
+ st.info("๐Ÿ‘‹ " + T('chat_welcome', lang))
1527
+
1528
+ user_input = st.chat_input(T('chat_input', lang))
1529
+ to_process = chip_clicked or user_input
1530
+ if to_process:
1531
+ st.session_state.chat_history.append({'role':'user','content':to_process})
1532
+ _chat_status_ph = st.empty()
1533
+ _spinner_label = {
1534
+ 'id': '๐Ÿค” Maya sedang berpikir...',
1535
+ 'en': '๐Ÿค” Maya is thinking...',
1536
+ 'hi': '๐Ÿค” Maya soch rahi hai...',
1537
+ }.get(lang, '๐Ÿค” Maya sedang berpikir...')
1538
+ with st.spinner(_spinner_label):
1539
+ _chat_status_ph.info(
1540
+ {'id':'โณ Menghubungi LLM โ€” bisa 5โ€“15 detik tergantung model yang dipilih...',
1541
+ 'en':'โณ Connecting to LLM โ€” may take 5โ€“15 seconds depending on the model...',
1542
+ 'hi':'โณ LLM se connect ho raha hai โ€” model ke hisaab se 5โ€“15 second lag sakte hain...'}[lang],
1543
+ icon="โณ"
1544
+ )
1545
+ ai_resp, adjustments, _debug_err = get_chat_response(
1546
+ to_process, st.session_state.chat_history,
1547
+ result, raw_input, shap_vals, feature_names, lang,
1548
+ rag_index=rag_index
1549
+ )
1550
+ _chat_status_ph.empty()
1551
+ if _debug_err and any([st.session_state.get("openrouter_key"),
1552
+ st.session_state.get("groq_key")]):
1553
+ st.warning("Semua LLM gagal, menggunakan Smart Fallback. Error: " + str(_debug_err)[:100])
1554
+ st.session_state.chat_history.append({'role':'assistant','content':ai_resp})
1555
+ _save_chat_memory(st.session_state.chat_history, st.session_state.get('chat_summary',''))
1556
+ adj_map = {
1557
+ 'digital_presence_score': 'wi_dig', 'business_age_years': 'wi_biz',
1558
+ 'num_employees': 'wi_emp', 'monthly_cash_flow': 'wi_cash',
1559
+ 'duration': 'wi_dur', 'loan_rp': 'wi_loan',
1560
+ }
1561
+ if adjustments:
1562
+ for field, val in adjustments.items():
1563
+ if field in adj_map:
1564
+ st.session_state[adj_map[field]] = val
1565
+ st.success("๐Ÿ’ก " + T('chat_updated', lang))
1566
+ st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1567
 
1568
+ if st.session_state.chat_history:
1569
+ if st.button(T('chat_clear', lang)):
1570
+ st.session_state.chat_history = []
1571
+ st.session_state.chat_summary = ''
1572
+ _save_chat_memory([], '')
1573
+ st.rerun()
1574
 
1575
  # โ”€โ”€ TAB 4: WHAT-IF โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1576
  with t4:
1577
  _tab_step(3)
 
1578
  @st.fragment
1579
  def render_whatif():
1580
  _lang = st.session_state.get("lang_sel","id")
 
1613
  'monthly_cash_flow': float(wi_cash * 1e6),
1614
  'num_employees': wi_emp,
1615
  }
1616
+ X_wi = preprocess(wi_raw, scaler, feature_names)
1617
+ wi_pd = float(ensemble.predict_proba(X_wi)[0][1])
1618
  wi_res = risk_result(wi_pd, wi_loan * 1e6, result_f['lgd'])
1619
+ d_pd = wi_pd - result_f['pd']
1620
+ d_el = wi_res['el'] - result_f['el']
1621
  _log_api("What-If", "Local Model", True, 0)
1622
 
1623
  r1, r2, r3 = st.columns(3)
 
1679
  with st.expander(T('wi_tips_title', _lang)):
1680
  tips = []
1681
  if wi_dig < 70:
1682
+ tips.append(_t("๐Ÿ“ฑ Naikkan Digital Score ke 70+ โ€” marketplace & Google Business",
1683
+ "๐Ÿ“ฑ Raise Digital Score to 70+ โ€” marketplace & Google Business",
1684
+ "๐Ÿ“ฑ Digital Score 70+ karein โ€” marketplace & Google Business"))
1685
  if wi_cash < 20:
1686
+ tips.append(_t("๐Ÿ’ต Target cash flow Rp 20jt+/bulan",
1687
+ "๐Ÿ’ต Target Rp 20M+/month cash flow",
1688
+ "๐Ÿ’ต Naqad pravaah Rp 20M+/maah target"))
1689
  if wi_biz < 3:
1690
+ tips.append(_t("๐Ÿข Bisnis < 3 thn lebih berisiko โ€” bangun track record",
1691
+ "๐Ÿข Business < 3 yr is riskier โ€” build track record",
1692
+ "๐Ÿข 3 saal se kam vyapaar โ€” track record banaen"))
1693
  if wi_emp < 5:
1694
+ tips.append(_t("๐Ÿ‘ฅ Tambah karyawan = skala bisnis lebih sehat",
1695
+ "๐Ÿ‘ฅ More employees signals healthy scale",
1696
+ "๐Ÿ‘ฅ Zyada karmachaaree = swasth paimaana"))
1697
  if not tips:
1698
+ tips.append(_t("๐ŸŒŸ Profil sudah optimal!",
1699
+ "๐ŸŒŸ Profile already well-optimized!",
1700
+ "๐ŸŒŸ Parichay pehle se anukoolit!"))
1701
  for tip in tips:
1702
  st.markdown(tip)
1703
 
 
1710
  with t5:
1711
  _tab_step(4)
1712
  st.markdown('<div class="sec-title">' + T("form_formula_title",lang) + '</div>', unsafe_allow_html=True)
1713
+ st.latex(r"EL = PD \times LGD \times EAD")
1714
+ st.latex(
1715
+ "EL = " + str(round(result['pd'],4)) + r" \times " + str(round(result['lgd'],2))
1716
+ + r" \times Rp\," + "{:,}".format(int(result['ead']))
1717
+ + r" = Rp\," + "{:,}".format(int(result['el']))
1718
+ )
1719
+ cf1, cf2 = st.columns(2)
1720
+ with cf1:
1721
+ st.markdown(T('formula_def', lang))
1722
+ with cf2:
1723
+ cat_lbl = (
1724
+ T('formula_low',lang) if result['pd'] < .2 else
1725
+ T('formula_medium',lang) if result['pd'] < .5 else
1726
+ T('formula_high',lang)
1727
  )
1728
+ st.markdown(
1729
+ "| " + T('formula_komponen',lang) + " | " + T('formula_nilai',lang) + " |\n"
1730
+ "|--|--|\n"
1731
+ "| PD | " + str(round(result['pd']*100,1)) + "% (" + cat_lbl + ") |\n"
1732
+ "| LGD | " + str(int(result['lgd']*100)) + "% |\n"
1733
+ "| EAD | Rp " + str(int(result['ead']/1e6)) + "jt |\n"
1734
+ "| **EL** | **Rp " + str(round(result['el']/1e6,2)) + "jt** |"
1735
+ )
1736
+ _log_api("Formula", "Local", True, 0)
 
 
 
 
 
 
 
 
 
1737
 
1738
  # ============================================================
1739
+ # DOWNLOAD REPORT
 
 
 
1740
  # ============================================================
1741
  _tab_step(5)
1742
  _tab_prog_ph.empty()
1743
+ st.markdown("---")
1744
+ report_txt = (
1745
+ "SME CREDIT RISK REPORT โ€” 1na37 AI ยท Batch 10\n" + "="*50 + "\n"
1746
+ + T('kpi_pd',lang) + ": " + str(round(result['pd']*100,1)) + "% โ†’ " + result['cat'][lang] + "\n"
1747
+ + T('kpi_el',lang) + ": Rp " + "{:,}".format(int(result['el'])) + "\n"
1748
+ "LGD: " + str(int(result['lgd']*100)) + "% (Basel II)\n"
1749
+ "EAD: Rp " + "{:,}".format(int(result['ead'])) + "\n"
1750
+ + "="*50 + "\n"
1751
+ + T('narr_title',lang) + "\n" + narrative + "\n"
1752
+ + "="*50 + "\n"
1753
+ "TOP SHAP\n" + shap_summary(shap_vals, feature_names, 5) + "\n"
1754
+ + "="*50 + "\n"
1755
+ "DISCLAIMER: Educational use only. Not financial advice."
1756
+ )
1757
+ st.download_button(
1758
+ T('download_btn', lang),
1759
+ data=report_txt,
1760
+ file_name=T('download_file', lang),
1761
+ mime="text/plain"
1762
+ )