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