Siddharaj Shirke commited on
Commit
67c8aca
·
1 Parent(s): cc728a5

v1 completed successfully

Browse files
.dockerignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ venv
4
+ .env
5
+ *.db
6
+ .git
7
+ .gitignore
8
+ __pycache__
9
+ *.pyc
10
+ .pytest_cache
11
+ .vscode
12
+ .idea
13
+ frontend/node_modules
14
+ frontend/dist
15
+ backend/__pycache__
.env.example ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NVIDIA NIM API Key (Required for AI Advisor)
2
+ NVIDIA_API_KEY=your_nvapi_key_here
3
+
4
+ # Hugging Face Token (Required for Persistent DB Backups)
5
+ HF_TOKEN=your_hf_token_here
6
+
7
+ # Database URL (Default for Docker/HF)
8
+ # SQLALCHEMY_DATABASE_URL=sqlite:////app/data/loan_db.db
9
+
10
+ # Deployment environment (production/development)
11
+ ENV=production
.gitignore ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ venv/
6
+ .env
7
+
8
+ # Database (Local)
9
+ *.db
10
+ !data/*.csv
11
+
12
+ # Node / Frontend
13
+ node_modules/
14
+ dist/
15
+ build/
16
+ .next/
17
+
18
+ # IDE / Editor
19
+ .vscode/
20
+ .idea/
21
+ *.swp
22
+ *.swo
23
+ .DS_Store
24
+
25
+ # Docker Local Testing
26
+ docker-compose.override.yml
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build React Frontend
2
+ FROM node:20-alpine AS build
3
+ WORKDIR /app/frontend
4
+ COPY frontend/package*.json ./
5
+ RUN npm install
6
+ COPY frontend .
7
+ RUN npm run build
8
+
9
+ # Stage 2: Serve with FastAPI (Hugging Face setup)
10
+ FROM python:3.11-slim
11
+
12
+ # Create a non-root user matching Hugging Face defaults (1000)
13
+ RUN useradd -m -u 1000 user
14
+ WORKDIR /app
15
+
16
+ # System dependencies for scientific packages
17
+ RUN apt-get update && apt-get install -y gcc g++ \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # Copy project metadata and backend source for package installation
21
+ COPY pyproject.toml .
22
+ COPY backend ./backend/
23
+ RUN pip install --no-cache-dir .
24
+
25
+ # Create necessary directories and set ownership
26
+ RUN mkdir -p /app/backend /data /app/frontend
27
+ RUN chown -R user:user /app /data
28
+
29
+ # Switch to the non-root user
30
+ USER user
31
+
32
+ # Copy pre-built frontend from stage 1
33
+ COPY --from=build --chown=user:user /app/frontend/dist /app/frontend/dist
34
+
35
+ # Copy legacy data for ML Engine
36
+ COPY --chown=user:user data ./data/
37
+
38
+ # Copy backend source
39
+ COPY --chown=user:user backend ./backend/
40
+
41
+ # Hugging Face deployment assumes we serve on 7860
42
+ EXPOSE 7860
43
+
44
+ # FastAPI startup
45
+ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,51 +1,124 @@
1
- # 🏦 Loan Approval Prediction System
2
 
3
- A Machine Learning-based web application that predicts whether a loan will be approved or rejected based on applicant details.
4
 
5
  ---
6
 
7
- ## 🚀 Features
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- * Simple and user-friendly interface using Streamlit
10
- * Real-time loan approval prediction
11
- * Machine Learning model (Random Forest)
12
- * Data preprocessing and feature engineering
13
- * Displays prediction with confidence score
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  ---
16
 
17
- ## 🛠️ Tech Stack
18
 
19
- * Python
20
- * Streamlit
21
- * Pandas
22
- * NumPy
23
- * Scikit-learn
24
 
25
- ---
 
 
 
 
26
 
27
- ## ⚙️ How It Works
 
28
 
29
- 1. User enters loan details (income, loan amount, etc.)
30
- 2. Data is processed and transformed
31
- 3. Machine Learning model makes prediction
32
- 4. Result is displayed as Approved or Rejected with confidence
33
 
34
- ---
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- ## 🌐 Future Scope
 
 
 
37
 
38
- * Convert into full-stack application (React + Flask)
39
- * Add database integration
40
- * Deploy on cloud platform
41
- * Improve model accuracy
42
 
43
  ---
44
 
45
- ## 📌 Conclusion
46
 
47
- This project demonstrates how Machine Learning can be used to automate and improve the loan approval process, making it faster and more efficient.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  ---
50
 
51
- If you like this project, feel free to give it a star!
 
 
 
 
 
 
 
 
1
+ # 🏦 Enterprise Loan AI: Adaptive Predictive Ecosystem
2
 
3
+ **Enterprise-grade financial decision engine combining Hybrid ML Logic with Mistral Large 3 Generative Insights.**
4
 
5
  ---
6
 
7
+ ## 🏗️ 1. System Architecture
8
+
9
+ The ecosystem operates as a high-fidelity diagnostic pipeline, ensuring mathematical rigor before AI interpretation.
10
+
11
+ ```mermaid
12
+ graph TD
13
+ User((User)) -->|Submit Application| UI(React/Vite Frontend)
14
+ UI -->|API POST /predict| API(FastAPI Backend)
15
+
16
+ subgraph "Hybrid Inference Engine"
17
+ API -->|1. Deterministic Map| ML(sklearn Random Forest)
18
+ ML -->|Probabilities| ADVISOR(NVIDIA NIM Mistral-3)
19
+ ADVISOR -->|2. Structured Narrative| RESPONSE(Final JSON Packet)
20
+ end
21
+
22
+ RESPONSE -->|Persistence| DB[(SQLite / PostgreSQL)]
23
+ RESPONSE -->|Visual Analytics| DASH(Radar & Distribution Charts)
24
+ DASH -->|Render| User
25
+ ```
26
 
27
+ ---
28
+
29
+ ## 📂 2. Repository Structure
30
+
31
+ ```text
32
+ ├── backend/ # FastAPI Application Source
33
+ │ ├── db/ # Persistence & Data Models
34
+ │ ├── logic/ # Deterministic ML Engine (sklearn)
35
+ │ ├── services/ # LLM Advisor (NVIDIA NIM Integration)
36
+ │ ├── main.py # API Entry Point & Startup Logic
37
+ │ └── requirements.txt # Python Dependencies
38
+ ├── data/ # Training datasets (CSV)
39
+ ├── frontend/ # React (Vite) Application
40
+ │ ├── src/
41
+ │ │ ├── components/ # Modular UI Components (Charts, Form, Sidebar)
42
+ │ │ ├── App.jsx # State-Based Navigation & Layout
43
+ │ │ └── index.css # Premium UI Design System
44
+ │ └── package.json # Node.js Dependencies
45
+ ├── Dockerfile # Multi-stage Production Build
46
+ ├── .dockerignore # Docker Build Exclusions
47
+ ├── .gitignore # Git Excluded Files
48
+ └── .env.example # Environment Management Template
49
+ ```
50
 
51
  ---
52
 
53
+ ## 🚀 3. End-to-End Setup Guide
54
 
55
+ ### **A. Local Development**
 
 
 
 
56
 
57
+ #### **1. Backend (Python 3.11+)**
58
+ ```powershell
59
+ # Create Virtual Environment
60
+ python -m venv venv
61
+ .\venv\Scripts\activate
62
 
63
+ # Install Project as Editable Package
64
+ pip install -e .
65
 
66
+ # Start Server
67
+ uvicorn backend.main:app --reload --port 8000
68
+ ```
 
69
 
70
+ #### **2. Frontend (Node.js 20+)**
71
+ ```powershell
72
+ cd frontend
73
+ npm install
74
+ npm run dev
75
+ ```
76
+
77
+ ### **B. Environment Configuration**
78
+ Create a `.env` file in the root directory:
79
+ ```env
80
+ # Required for AI Analysis
81
+ NVIDIA_API_KEY=your_key_here
82
 
83
+ # Required for Persistent Cloud Backups (Optional)
84
+ HF_TOKEN=your_huggingface_write_token
85
+ HF_REPO_ID=your_username/your_space_name
86
+ ```
87
 
88
+ ### **C. Cloud Synchronization (New)**
89
+ The system now features an **Automated Cloud Sync** service.
90
+ - When running in a Docker/Hugging Face environment with a valid `HF_TOKEN`, the system will automatically back up your assessment history to your Space's repository every time you make a new prediction or clear the history.
91
+ - This ensures your data persists even if the Space's ephemeral container is restarted.
92
 
93
  ---
94
 
95
+ ## 🐳 4. Production Deployment (Hugging Face)
96
 
97
+ This project is optimized for deployment as a **Hugging Face Space** using the **Docker** runtime.
98
+
99
+ ### **1. Build & Run Locally**
100
+ ```powershell
101
+ # Build Image
102
+ docker build -t loan-prediction-app .
103
+
104
+ # Run Container (History persistence requires /app/data volume)
105
+ docker run -d -p 7860:7860 --name loan-app loan-prediction-app
106
+ ```
107
+
108
+ ### **2. Deploy to Hugging Face**
109
+ 1. Create a new Space on [huggingface.co](https://huggingface.co/spaces) selecting the **Docker** SDK.
110
+ 2. In the Space **Settings**:
111
+ - Add your `NVIDIA_API_KEY` as a **Secret**.
112
+ - (Optional) Enable **Persistent Storage** and mount to `/app/data`.
113
+ 3. Push your code. The Space will automatically build and launch your dashboard.
114
 
115
  ---
116
 
117
+ ## 5. Platform Features
118
+ - **Deterministic Math**: Validated Random Forest scoring.
119
+ - **AI Narrative Sub-Cards**: Readable, point-by-point financial insights.
120
+ - **Radar Comparisons**: Real-time benchmarking against successful profiles.
121
+ - **ChatGPT History Sidebar**: Persistent task tracking with "Clear History" support.
122
+
123
+ ---
124
+ *Built for High-Trust Lending Environments.*
assets/about.png DELETED
Binary file (95.2 kB)
 
assets/home.png DELETED
Binary file (79.8 kB)
 
assets/predict.png DELETED
Binary file (44.7 kB)
 
backend/db/database.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import sessionmaker, declarative_base
3
+
4
+ import os
5
+
6
+ # Persistent path for Docker/Hugging Face Spaces (Matches Bucket Mount)
7
+ DATA_DIR = "/data"
8
+ DB_PATH = os.path.join(DATA_DIR, "loan_db.db")
9
+
10
+ # Fallback to local root if not in Docker environment
11
+ if not os.path.exists(DATA_DIR):
12
+ DB_PATH = "./loan_db_v3.db"
13
+
14
+ SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}"
15
+
16
+ engine = create_engine(
17
+ SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
18
+ )
19
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
20
+
21
+ Base = declarative_base()
22
+
23
+ # Dependency
24
+ def get_db():
25
+ db = SessionLocal()
26
+ try:
27
+ yield db
28
+ finally:
29
+ db.close()
backend/db/models.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Float, DateTime, Text
2
+ from sqlalchemy.sql import func
3
+ from .database import Base
4
+
5
+ class LoanApplication(Base):
6
+ __tablename__ = "loan_applications"
7
+
8
+ id = Column(Integer, primary_key=True, index=True)
9
+ applicant_name = Column(String, default="Applicant")
10
+
11
+ # Financial Inputs (Stored as stringified JSON or separate columns, but columns are safer for SQLite)
12
+ gender = Column(String)
13
+ married = Column(String)
14
+ dependents = Column(String)
15
+ education = Column(String)
16
+ self_employed = Column(String)
17
+ applicant_income = Column(Float)
18
+ coapplicant_income = Column(Float)
19
+ loan_amount = Column(Float)
20
+ loan_amount_term = Column(Float)
21
+ credit_history = Column(Float)
22
+ property_area = Column(String)
23
+
24
+ # Processed Results
25
+ prediction = Column(String) # "Y" or "N"
26
+ confidence = Column(Float)
27
+ dti_ratio = Column(Float)
28
+
29
+ # AI & Explanation Outputs
30
+ explanation_text = Column(Text, nullable=True)
31
+ optimized_suggestion = Column(Text, nullable=True)
32
+ feature_importance_json = Column(Text, nullable=True) # stringified JSON
33
+ benchmarks_json = Column(Text, nullable=True) # stringified JSON
34
+
35
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
backend/db/schemas.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ class LoanApplicationBase(BaseModel):
6
+ applicant_name: Optional[str] = "Applicant"
7
+ gender: str = Field(..., description="Male or Female")
8
+ married: str = Field(..., description="Yes or No")
9
+ dependents: str = Field(..., description="0, 1, 2, 3+")
10
+ education: str = Field(..., description="Graduate or Not Graduate")
11
+ self_employed: str = Field(..., description="Yes or No")
12
+ applicant_income: float = Field(..., description="Monthly Applicant Income")
13
+ coapplicant_income: float = Field(..., description="Monthly Co-applicant Income")
14
+ loan_amount: float = Field(..., description="Total Loan AmountRequested")
15
+ loan_amount_term: float = Field(..., description="Loan Term in Months")
16
+ credit_history: float = Field(..., description="1.0 for Yes, 0.0 for No")
17
+ property_area: str = Field(..., description="Urban, Semiurban, Rural")
18
+
19
+ class LoanApplicationCreate(LoanApplicationBase):
20
+ pass
21
+
22
+ class LoanApplicationResponse(LoanApplicationBase):
23
+ id: int
24
+ prediction: str
25
+ confidence: float
26
+ dti_ratio: float
27
+ explanation_text: Optional[str] = None
28
+ optimized_suggestion: Optional[str] = None
29
+ feature_importance_json: Optional[str] = None
30
+ benchmarks_json: Optional[str] = None
31
+ created_at: datetime
32
+
33
+ class Config:
34
+ from_attributes = True
backend/logic/deterministic.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from .ml_model import ml_engine
3
+ import json
4
+
5
+ KEY_MAP = {
6
+ "gender": "Gender",
7
+ "married": "Married",
8
+ "dependents": "Dependents",
9
+ "education": "Education",
10
+ "self_employed": "Self_Employed",
11
+ "applicant_income": "ApplicantIncome",
12
+ "coapplicant_income": "CoapplicantIncome",
13
+ "loan_amount": "LoanAmount",
14
+ "loan_amount_term": "Loan_Amount_Term",
15
+ "credit_history": "Credit_History",
16
+ "property_area": "Property_Area"
17
+ }
18
+
19
+ def map_keys(data: dict) -> dict:
20
+ return {KEY_MAP.get(k, k): v for k, v in data.items()}
21
+
22
+ def prepare_input(base_data: dict) -> dict:
23
+ # Compute derived features for the ML Engine
24
+ total_income = base_data["ApplicantIncome"] + base_data["CoapplicantIncome"]
25
+
26
+ # Avoid division by zero
27
+ term = base_data["Loan_Amount_Term"] if base_data["Loan_Amount_Term"] > 0 else 360
28
+
29
+ emi = (base_data["LoanAmount"] * 1000) / term
30
+ balance_income = total_income - emi
31
+
32
+ base_data["Total_Income"] = total_income
33
+ base_data["EMI"] = emi
34
+ base_data["Balance_Income"] = balance_income
35
+ return base_data
36
+
37
+ def process_pipeline(input_data: dict) -> dict:
38
+ """Evaluates the loan application and runs sensitivity analysis if rejected."""
39
+
40
+ # 0. Map snake_case API keys to PascalCase Model keys
41
+ mapped_input = map_keys(input_data)
42
+
43
+ # 1. Base Evaluation
44
+ processed_input = prepare_input(copy.deepcopy(mapped_input))
45
+
46
+ result, confidence = ml_engine.predict(processed_input)
47
+
48
+ dti_ratio = 0.0
49
+ if processed_input["Total_Income"] > 0:
50
+ dti_ratio = processed_input["EMI"] / processed_input["Total_Income"]
51
+ dti_ratio_pct = min(100.0, dti_ratio * 100)
52
+
53
+ response = {
54
+ "prediction": result,
55
+ "confidence": confidence,
56
+ "dti_ratio": dti_ratio_pct,
57
+ "optimized_suggestion": "Your profile meets all current thresholds. No optimization necessary.",
58
+ "feature_importances": ml_engine.get_feature_importances(),
59
+ "benchmarks": ml_engine.get_benchmarks()
60
+ }
61
+
62
+ # 2. What-If Optimization (If Rejected)
63
+ if result == "N":
64
+ # Simulate lower loan amount
65
+ sim_data_loan = copy.deepcopy(mapped_input)
66
+ loan_decrease_needed = 0
67
+ loan_approved = False
68
+
69
+ while sim_data_loan["LoanAmount"] > 10 and not loan_approved:
70
+ sim_data_loan["LoanAmount"] -= 10 # Decrease by 10k
71
+ loan_decrease_needed += 10
72
+
73
+ p_input = prepare_input(copy.deepcopy(sim_data_loan))
74
+ sim_res, sim_conf = ml_engine.predict(p_input)
75
+ if sim_res == "Y":
76
+ loan_approved = True
77
+ break
78
+
79
+ # Simulate higher coapplicant income
80
+ sim_data_inc = copy.deepcopy(mapped_input)
81
+ inc_increase_needed = 0
82
+ inc_approved = False
83
+
84
+ while inc_increase_needed < 20000 and not inc_approved:
85
+ sim_data_inc["CoapplicantIncome"] += 1000 # Increase by 1k
86
+ inc_increase_needed += 1000
87
+
88
+ p_input_inc = prepare_input(copy.deepcopy(sim_data_inc))
89
+ sim_res_inc, sim_conf_inc = ml_engine.predict(p_input_inc)
90
+ if sim_res_inc == "Y":
91
+ inc_approved = True
92
+ break
93
+
94
+ # Generate rule-based optimization text (Determnistic)
95
+ suggestion = "Unfortunately, we couldn't find a minor adjustment to approve your loan. Consider improving your CIBIL score."
96
+
97
+ if loan_approved and inc_approved:
98
+ suggestion = f"Reducing your loan request by ₹{loan_decrease_needed * 1000} OR adding a co-applicant income of ~₹{inc_increase_needed} would likely result in approval."
99
+ elif loan_approved:
100
+ suggestion = f"Reducing your loan request by ₹{loan_decrease_needed * 1000} would likely result in an approval."
101
+ elif inc_approved:
102
+ suggestion = f"Adding a co-applicant income of ~₹{inc_increase_needed} would likely result in an approval."
103
+
104
+ if mapped_input["Credit_History"] == 0:
105
+ suggestion = "Your Credit History is 0. This is the primary blocking factor. Improving your credit standing is required before other adjustments will work."
106
+
107
+ response["optimized_suggestion"] = suggestion
108
+
109
+ return response
backend/logic/ml_model.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ import numpy as np
4
+ from sklearn.ensemble import RandomForestClassifier
5
+ from sklearn.preprocessing import LabelEncoder
6
+ import json
7
+
8
+ class MLEngine:
9
+ def __init__(self):
10
+ self.model = None
11
+ self.encoders = {}
12
+ self.target_encoder = None
13
+ self.feature_columns = []
14
+ self.train_model()
15
+
16
+ def load_data(self):
17
+ path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "..", "data", "train.csv")
18
+ try:
19
+ df = pd.read_csv(path)
20
+ for col in df.select_dtypes(include=['int64', 'float64']).columns:
21
+ df[col] = df[col].fillna(df[col].mean())
22
+ for col in df.select_dtypes(include=['object']).columns:
23
+ df[col] = df[col].fillna(df[col].mode()[0])
24
+ return df
25
+ except Exception as e:
26
+ print(f"Error loading {path}: {e}")
27
+ return None
28
+
29
+ def feature_engineering(self, df):
30
+ if "Loan_ID" in df.columns:
31
+ df = df.drop("Loan_ID", axis=1)
32
+ df["Total_Income"] = df["ApplicantIncome"] + df["CoapplicantIncome"]
33
+ df["EMI"] = (df["LoanAmount"] * 1000) / df["Loan_Amount_Term"]
34
+ df["Balance_Income"] = df["Total_Income"] - df["EMI"]
35
+ return df
36
+
37
+ def encode_data(self, df):
38
+ encoders = {}
39
+ target_encoder = LabelEncoder()
40
+
41
+ # Avoid SettingWithCopyWarning by operating on frame directly if needed, but it's okay here
42
+ df["Loan_Status"] = target_encoder.fit_transform(df["Loan_Status"])
43
+
44
+ cols = ["Gender", "Married", "Dependents", "Education", "Self_Employed", "Property_Area"]
45
+ for col in cols:
46
+ le = LabelEncoder()
47
+ df[col] = le.fit_transform(df[col].astype(str))
48
+ encoders[col] = le
49
+
50
+ return df, encoders, target_encoder
51
+
52
+ def train_model(self):
53
+ print("Training Random Forest model on boot...")
54
+ df = self.load_data()
55
+ if df is not None:
56
+ df = self.feature_engineering(df)
57
+ df, self.encoders, self.target_encoder = self.encode_data(df)
58
+
59
+ X = df.drop("Loan_Status", axis=1)
60
+ y = df["Loan_Status"]
61
+ self.feature_columns = X.columns.tolist()
62
+
63
+ self.model = RandomForestClassifier(n_estimators=200, max_depth=10, random_state=42)
64
+ self.model.fit(X, y)
65
+
66
+ # Calculate Benchmarks for 'Approved' (Status 1 if transformed or 'Y' if checking pre-encoded)
67
+ # Find the index of 'Y' in target_encoder
68
+ y_idx = list(self.target_encoder.classes_).index('Y')
69
+ approved_df = df[df['Loan_Status'] == y_idx]
70
+ self.benchmarks = approved_df[self.feature_columns].mean().to_dict()
71
+
72
+ print("Model and Benchmarks generated successfully.")
73
+
74
+ def get_benchmarks(self):
75
+ return {k: float(v) for k, v in getattr(self, 'benchmarks', {}).items()}
76
+
77
+ def get_feature_importances(self):
78
+ if not self.model: return {}
79
+ importances = self.model.feature_importances_
80
+ # Convert numpy types to standard python floats for JSON serialization
81
+ return {k: float(v) for k, v in zip(self.feature_columns, importances)}
82
+
83
+ def predict(self, input_data: dict):
84
+ if not self.model:
85
+ raise Exception("Model is not trained.")
86
+
87
+ df = pd.DataFrame([input_data])
88
+ for col in self.feature_columns:
89
+ if col not in df.columns:
90
+ df[col] = 0
91
+
92
+ df = df[self.feature_columns]
93
+
94
+ for col, le in self.encoders.items():
95
+ if col in df.columns:
96
+ df[col] = le.transform(df[col].astype(str))
97
+
98
+ pred = self.model.predict(df)
99
+ prob = self.model.predict_proba(df)
100
+
101
+ result = self.target_encoder.inverse_transform(pred)[0]
102
+ confidence = float(np.max(prob))
103
+
104
+ return result, confidence
105
+
106
+ ml_engine = MLEngine()
backend/main.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import FileResponse
5
+ from sqlalchemy.orm import Session
6
+ from sqlalchemy import desc
7
+ import json
8
+ import os
9
+
10
+ from backend.db import database, models, schemas
11
+ from backend.logic.deterministic import process_pipeline
12
+ from backend.services.llm_advisor import generate_explanation
13
+ from backend.services.hf_sync import sync_db_to_hf
14
+
15
+ # Create tables
16
+ models.Base.metadata.create_all(bind=database.engine)
17
+
18
+ app = FastAPI(title="Loan Prediction System API")
19
+
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["*"], # Allow all for demo
23
+ allow_credentials=True,
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+ @app.post("/api/predict", response_model=schemas.LoanApplicationResponse)
29
+ def create_prediction(app_in: schemas.LoanApplicationCreate, background_tasks: BackgroundTasks, db: Session = Depends(database.get_db)):
30
+ # 1. Convert to dictionary
31
+ input_data = app_in.model_dump()
32
+
33
+ # 2. Run Deterministic Layer (Prediction + Wait-If Simulation)
34
+ packet = process_pipeline(input_data)
35
+
36
+ # 3. Pass Packet to LLM Layer
37
+ explanation = generate_explanation(packet)
38
+
39
+ # 4. Save to DB
40
+ db_record = models.LoanApplication(
41
+ applicant_name=app_in.applicant_name,
42
+ gender=app_in.gender,
43
+ married=app_in.married,
44
+ dependents=app_in.dependents,
45
+ education=app_in.education,
46
+ self_employed=app_in.self_employed,
47
+ applicant_income=app_in.applicant_income,
48
+ coapplicant_income=app_in.coapplicant_income,
49
+ loan_amount=app_in.loan_amount,
50
+ loan_amount_term=app_in.loan_amount_term,
51
+ credit_history=app_in.credit_history,
52
+ property_area=app_in.property_area,
53
+
54
+ prediction=packet["prediction"],
55
+ confidence=packet["confidence"],
56
+ dti_ratio=packet["dti_ratio"],
57
+
58
+ explanation_text=explanation,
59
+ optimized_suggestion=packet["optimized_suggestion"],
60
+ feature_importance_json=json.dumps(packet["feature_importances"]),
61
+ benchmarks_json=json.dumps(packet["benchmarks"])
62
+ )
63
+
64
+ db.add(db_record)
65
+ db.commit()
66
+ db.refresh(db_record)
67
+
68
+ # Trigger HF Backup in background
69
+ background_tasks.add_task(sync_db_to_hf)
70
+
71
+ return db_record
72
+
73
+ @app.get("/api/history", response_model=list[schemas.LoanApplicationResponse])
74
+ def get_history(limit: int = 10, db: Session = Depends(database.get_db)):
75
+ records = db.query(models.LoanApplication).order_by(desc(models.LoanApplication.created_at)).limit(limit).all()
76
+ return records
77
+
78
+ @app.delete("/api/history")
79
+ def clear_history(background_tasks: BackgroundTasks, db: Session = Depends(database.get_db)):
80
+ db.query(models.LoanApplication).delete()
81
+ db.commit()
82
+
83
+ # Trigger HF Backup in background
84
+ background_tasks.add_task(sync_db_to_hf)
85
+
86
+ return {"message": "History cleared successfully"}
87
+
88
+ # Serve Frontend
89
+ frontend_build_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist")
90
+ if os.path.isdir(frontend_build_path):
91
+ app.mount("/assets", StaticFiles(directory=os.path.join(frontend_build_path, "assets")), name="assets")
92
+
93
+ @app.get("/{full_path:path}")
94
+ def catch_all(full_path: str):
95
+ index_path = os.path.join(frontend_build_path, "index.html")
96
+ if os.path.isfile(index_path):
97
+ return FileResponse(index_path)
98
+ return {"error": "Frontend build not found."}
backend/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.100.0
2
+ uvicorn>=0.23.0
3
+ pydantic>=2.0.0
4
+ sqlalchemy>=2.0.0
5
+ openai>=1.0.0
6
+ scikit-learn>=1.2.0
7
+ pandas>=2.0.0
8
+ numpy>=1.24.0
9
+ python-dotenv>=1.0.0
10
+ huggingface_hub>=0.19.0
backend/services/hf_sync.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from huggingface_hub import HfApi
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ def sync_db_to_hf():
8
+ """
9
+ Syncs the local SQLite database to a Hugging Face repository.
10
+ This is useful for persisting data on ephemeral Hugging Face Spaces.
11
+ """
12
+ token = os.getenv("HF_TOKEN")
13
+ repo_id = os.getenv("HF_REPO_ID") # e.g., "username/space-name"
14
+
15
+ # Hugging Face Spaces automatically provide SPACE_ID
16
+ if not repo_id:
17
+ repo_id = os.getenv("SPACE_ID")
18
+
19
+ if not token or not repo_id:
20
+ print("HF_Sync: Missing HF_TOKEN or HF_REPO_ID. Skipping sync.")
21
+ return False
22
+
23
+ # Get the db path from database.py logic
24
+ # We'll just use the constant path if it exists
25
+ db_path = "/app/data/loan_db.db"
26
+
27
+ # Fallback to local if not in docker
28
+ if not os.path.exists("/app/data"):
29
+ db_path = "./loan_db_v3.db"
30
+
31
+ if not os.path.exists(db_path):
32
+ print(f"HF_Sync: Database file not found at {db_path}. Skipping.")
33
+ return False
34
+
35
+ try:
36
+ api = HfApi(token=token)
37
+ print(f"HF_Sync: Uploading {db_path} to {repo_id}...")
38
+
39
+ api.upload_file(
40
+ path_or_fileobj=db_path,
41
+ path_in_repo="data/loan_db_backup.db",
42
+ repo_id=repo_id,
43
+ repo_type="space" # Assuming it's a space, but could be 'dataset'
44
+ )
45
+ print("HF_Sync: Database successfully synced to Hugging Face Space.")
46
+ return True
47
+ except Exception as e:
48
+ print(f"HF_Sync: Error during synchronization: {e}")
49
+ return False
backend/services/llm_advisor.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ SYSTEM_PROMPT = """
8
+ You are a senior banking AI advisor for a high-end lending firm.
9
+ Your goal is to provide a very professional, encouraging, and data-driven explanation for a loan decision.
10
+
11
+ Rules:
12
+ 1. Divide your explanation into exactly 3 or 4 clear, distinct points.
13
+ 2. Prefix EVERY point with the delimiter: @@POINT@@
14
+ 3. Keep each point concise (1-2 sentences).
15
+ 4. Do NOT use markdown headers like ###. Use **Bold** for emphasis.
16
+ 5. Do NOT invent new financial data. Only use what is provided in the packet.
17
+ 6. If the loan is rejected, ensure the points explain the 'Why' and refer to the specific 'Actionable Step' provided in the deterministic packet.
18
+
19
+ Format:
20
+ @@POINT@@ [First insight]
21
+ @@POINT@@ [Second insight]
22
+ ...etc
23
+ """
24
+
25
+ # Prioritized models for fallback logic
26
+ LLM_MODELS = [
27
+ "mistralai/mistral-large-3-675b-instruct-2512",
28
+ "meta/llama-3.1-405b-instruct",
29
+ "mistralai/mixtral-8x7b-instruct-v0.1"
30
+ ]
31
+
32
+ def generate_explanation(packet: dict) -> str:
33
+ api_key = os.getenv("NVIDIA_API_KEY")
34
+
35
+ # Check if we should use Deterministic Mode
36
+ result_label = "Approved" if packet.get('prediction') == 'Y' else "Rejected"
37
+ confidence_val = packet.get('confidence', 0) * 100
38
+ dti_val = packet.get('dti_ratio', 0)
39
+ suggestion = packet.get('optimized_suggestion', '')
40
+
41
+ default_narrative = f"""
42
+ ### **AI Advisor Analysis (Deterministic Mode)**
43
+ *Note: This is an automated diagnostic analysis.*
44
+
45
+ Based on our core financial engine, your application has been **{result_label}** with a confidence score of **{confidence_val:.2f}%**.
46
+
47
+ **Key Insights:**
48
+ - Your current **Debt-to-Income (DTI)** ratio is **{dti_val:.2f}%**.
49
+ - **Actionable Step:** {suggestion}
50
+ """
51
+
52
+ if not api_key or "YOUR_KEY" in api_key:
53
+ return default_narrative
54
+
55
+ client = OpenAI(
56
+ base_url="https://integrate.api.nvidia.com/v1",
57
+ api_key=api_key
58
+ )
59
+
60
+ user_content = f"""
61
+ Result: {packet.get('prediction', 'Unknown')}
62
+ Confidence: {packet.get('confidence', 0):.2f}%
63
+ DTI Ratio: {packet.get('dti_ratio', 0):.2f}%
64
+ Primary Suggestion: {packet.get('optimized_suggestion', 'None')}
65
+ """
66
+
67
+ # Multi-model Fallback Loop
68
+ for model_name in LLM_MODELS:
69
+ try:
70
+ print(f"Attempting inference with model: {model_name}...")
71
+ completion = client.chat.completions.create(
72
+ model=model_name,
73
+ messages=[
74
+ {"role": "system", "content": SYSTEM_PROMPT},
75
+ {"role": "user", "content": user_content}
76
+ ],
77
+ temperature=0.15,
78
+ max_tokens=1024
79
+ )
80
+
81
+ content = completion.choices[0].message.content
82
+ # Basic validation that model followed instructions
83
+ if "@@POINT@@" in content:
84
+ print(f"Success with model: {model_name}")
85
+ return content
86
+ else:
87
+ print(f"Model {model_name} failed to provide structured output. Trying next...")
88
+
89
+ except Exception as e:
90
+ print(f"Error calling {model_name}: {e}. Retrying with fallback...")
91
+
92
+ # Final Fallback if all AI models fail
93
+ print("CRITICAL: All AI models failed. Returning deterministic narrative.")
94
+ return default_narrative
backend/tests/test_llm_fallback.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from dotenv import load_dotenv
4
+ from openai import OpenAI
5
+
6
+ # Add project root to path
7
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
+ from services.llm_advisor import generate_explanation, LLM_MODELS, SYSTEM_PROMPT
9
+
10
+ load_dotenv()
11
+
12
+ def test_single_model(model_name, packet):
13
+ print(f"\n--- Testing Model: {model_name} ---")
14
+ api_key = os.getenv("NVIDIA_API_KEY")
15
+ client = OpenAI(base_url="https://integrate.api.nvidia.com/v1", api_key=api_key)
16
+
17
+ user_content = f"Result: {packet['prediction']}, Confidence: {packet['confidence']}, DTI: {packet['dti_ratio']}"
18
+
19
+ try:
20
+ completion = client.chat.completions.create(
21
+ model=model_name,
22
+ messages=[
23
+ {"role": "system", "content": SYSTEM_PROMPT},
24
+ {"role": "user", "content": user_content}
25
+ ],
26
+ temperature=0.1,
27
+ max_tokens=512
28
+ )
29
+ content = completion.choices[0].message.content
30
+ print(f"Response received ({len(content)} chars)")
31
+ if "@@POINT@@" in content:
32
+ print("SUCCESS: Output contains required @@POINT@@ delimiters.")
33
+ else:
34
+ print("FAILURE: Output missing @@POINT@@ delimiters.")
35
+ return True
36
+ except Exception as e:
37
+ print(f"ERROR: Model {model_name} failed: {e}")
38
+ return False
39
+
40
+ def test_fallback_logic(packet):
41
+ print("\n--- Testing Full Fallback Workflow ---")
42
+ # Forcing a failure by temporarily corrupting the model list in memory
43
+ original_models = list(LLM_MODELS)
44
+ # Inject a fake non-existent model at the start
45
+ import services.llm_advisor as advisor
46
+ advisor.LLM_MODELS = ["invalid/non-existent-model-1234"] + original_models
47
+
48
+ print("Injected invalid model. Expecting fail-over to the first valid model...")
49
+ response = advisor.generate_explanation(packet)
50
+
51
+ if "@@POINT@@" in response:
52
+ print("SUCCESS: Fallback logic correctly skipped the invalid model and used a secondary model.")
53
+ else:
54
+ print("FAILURE: Fallback logic did not return a valid AI summary.")
55
+
56
+ # Reset models
57
+ advisor.LLM_MODELS = original_models
58
+
59
+ if __name__ == "__main__":
60
+ sample_packet = {
61
+ "prediction": "Y",
62
+ "confidence": 0.88,
63
+ "dti_ratio": 32.5,
64
+ "optimized_suggestion": "Excellent profile."
65
+ }
66
+
67
+ print("STARTING COMPREHENSIVE LLM VERIFICATION")
68
+
69
+ # 1. Test every model in the list
70
+ results = []
71
+ for m in LLM_MODELS:
72
+ results.append(test_single_model(m, sample_packet))
73
+
74
+ # 2. Test the fallback mechanism
75
+ test_fallback_logic(sample_packet)
76
+
77
+ print("\nVERIFICATION COMPLETE")
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.15.0",
14
+ "chart.js": "^4.5.1",
15
+ "lucide-react": "^1.8.0",
16
+ "react": "^19.2.4",
17
+ "react-chartjs-2": "^5.3.1",
18
+ "react-dom": "^19.2.4",
19
+ "react-markdown": "^10.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^9.39.4",
23
+ "@types/react": "^19.2.14",
24
+ "@types/react-dom": "^19.2.3",
25
+ "@vitejs/plugin-react": "^6.0.1",
26
+ "eslint": "^9.39.4",
27
+ "eslint-plugin-react-hooks": "^7.0.1",
28
+ "eslint-plugin-react-refresh": "^0.5.2",
29
+ "globals": "^17.4.0",
30
+ "vite": "^8.0.4"
31
+ }
32
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .counter {
2
+ font-size: 16px;
3
+ padding: 5px 10px;
4
+ border-radius: 5px;
5
+ color: var(--accent);
6
+ background: var(--accent-bg);
7
+ border: 2px solid transparent;
8
+ transition: border-color 0.3s;
9
+ margin-bottom: 24px;
10
+
11
+ &:hover {
12
+ border-color: var(--accent-border);
13
+ }
14
+ &:focus-visible {
15
+ outline: 2px solid var(--accent);
16
+ outline-offset: 2px;
17
+ }
18
+ }
19
+
20
+ .hero {
21
+ position: relative;
22
+
23
+ .base,
24
+ .framework,
25
+ .vite {
26
+ inset-inline: 0;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .base {
31
+ width: 170px;
32
+ position: relative;
33
+ z-index: 0;
34
+ }
35
+
36
+ .framework,
37
+ .vite {
38
+ position: absolute;
39
+ }
40
+
41
+ .framework {
42
+ z-index: 1;
43
+ top: 34px;
44
+ height: 28px;
45
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
46
+ scale(1.4);
47
+ }
48
+
49
+ .vite {
50
+ z-index: 0;
51
+ top: 107px;
52
+ height: 26px;
53
+ width: auto;
54
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
55
+ scale(0.8);
56
+ }
57
+ }
58
+
59
+ #center {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 25px;
63
+ place-content: center;
64
+ place-items: center;
65
+ flex-grow: 1;
66
+
67
+ @media (max-width: 1024px) {
68
+ padding: 32px 20px 24px;
69
+ gap: 18px;
70
+ }
71
+ }
72
+
73
+ #next-steps {
74
+ display: flex;
75
+ border-top: 1px solid var(--border);
76
+ text-align: left;
77
+
78
+ & > div {
79
+ flex: 1 1 0;
80
+ padding: 32px;
81
+ @media (max-width: 1024px) {
82
+ padding: 24px 20px;
83
+ }
84
+ }
85
+
86
+ .icon {
87
+ margin-bottom: 16px;
88
+ width: 22px;
89
+ height: 22px;
90
+ }
91
+
92
+ @media (max-width: 1024px) {
93
+ flex-direction: column;
94
+ text-align: center;
95
+ }
96
+ }
97
+
98
+ #docs {
99
+ border-right: 1px solid var(--border);
100
+
101
+ @media (max-width: 1024px) {
102
+ border-right: none;
103
+ border-bottom: 1px solid var(--border);
104
+ }
105
+ }
106
+
107
+ #next-steps ul {
108
+ list-style: none;
109
+ padding: 0;
110
+ display: flex;
111
+ gap: 8px;
112
+ margin: 32px 0 0;
113
+
114
+ .logo {
115
+ height: 18px;
116
+ }
117
+
118
+ a {
119
+ color: var(--text-h);
120
+ font-size: 16px;
121
+ border-radius: 6px;
122
+ background: var(--social-bg);
123
+ display: flex;
124
+ padding: 6px 12px;
125
+ align-items: center;
126
+ gap: 8px;
127
+ text-decoration: none;
128
+ transition: box-shadow 0.3s;
129
+
130
+ &:hover {
131
+ box-shadow: var(--shadow);
132
+ }
133
+ .button-icon {
134
+ height: 18px;
135
+ width: 18px;
136
+ }
137
+ }
138
+
139
+ @media (max-width: 1024px) {
140
+ margin-top: 20px;
141
+ flex-wrap: wrap;
142
+ justify-content: center;
143
+
144
+ li {
145
+ flex: 1 1 calc(50% - 8px);
146
+ }
147
+
148
+ a {
149
+ width: 100%;
150
+ justify-content: center;
151
+ box-sizing: border-box;
152
+ }
153
+ }
154
+ }
155
+
156
+ #spacer {
157
+ height: 88px;
158
+ border-top: 1px solid var(--border);
159
+ @media (max-width: 1024px) {
160
+ height: 48px;
161
+ }
162
+ }
163
+
164
+ .ticks {
165
+ position: relative;
166
+ width: 100%;
167
+
168
+ &::before,
169
+ &::after {
170
+ content: '';
171
+ position: absolute;
172
+ top: -4.5px;
173
+ border: 5px solid transparent;
174
+ }
175
+
176
+ &::before {
177
+ left: 0;
178
+ border-left-color: var(--border);
179
+ }
180
+ &::after {
181
+ right: 0;
182
+ border-right-color: var(--border);
183
+ }
184
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import Form from './components/Form';
3
+ import Dashboard from './components/Dashboard';
4
+ import Sidebar from './components/Sidebar';
5
+ import Home from './components/Home';
6
+
7
+ import { Home as HomeIcon } from 'lucide-react';
8
+
9
+ function App() {
10
+ const [view, setView] = useState('home'); // 'home', 'form', 'result'
11
+ const [result, setResult] = useState(null);
12
+ const [loading, setLoading] = useState(false);
13
+ const [historyKey, setHistoryKey] = useState(0);
14
+
15
+ const handleProceed = () => setView('form');
16
+
17
+ const handleNewAssessment = () => {
18
+ setResult(null);
19
+ setView('form');
20
+ };
21
+
22
+ const handleSelectResult = (pastResult) => {
23
+ setResult(pastResult);
24
+ setView('result');
25
+ };
26
+
27
+ const handleNewResult = (newResult) => {
28
+ setResult(newResult);
29
+ setView('result');
30
+ setHistoryKey(k => k + 1); // Refresh sidebar history
31
+ };
32
+
33
+ const handleBackToHome = () => setView('home');
34
+
35
+ return (
36
+ <div className="app-layout">
37
+ {/* Sidebar only visible on Form and Result pages */}
38
+ {(view === 'form' || view === 'result') && (
39
+ <Sidebar
40
+ key={historyKey}
41
+ onSelectResult={handleSelectResult}
42
+ onNewAssessment={handleNewAssessment}
43
+ />
44
+ )}
45
+
46
+ <div className="main-content" style={{ position: 'relative' }}>
47
+ {/* Navigation Button at Top Right corner of all pages except Home */}
48
+ {view !== 'home' && (
49
+ <button className="back-to-home-btn" onClick={handleBackToHome}>
50
+ <HomeIcon size={16} /> Back to Home
51
+ </button>
52
+ )}
53
+
54
+ <div className="content-container">
55
+ {loading ? (
56
+ <div style={{ textAlign: 'center', padding: '6rem 0' }}>
57
+ <div style={{ fontSize: '1.5rem', color: 'var(--primary)', fontWeight: '600' }}>Analyzing application data...</div>
58
+ </div>
59
+ ) : view === 'home' ? (
60
+ <Home onProceed={handleProceed} />
61
+ ) : view === 'form' ? (
62
+ <Form setResult={handleNewResult} setLoading={setLoading} />
63
+ ) : view === 'result' && result ? (
64
+ <Dashboard result={result} onReset={handleNewAssessment} />
65
+ ) : null}
66
+ </div>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ export default App;
frontend/src/assets/hero.png ADDED
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/components/Charts.jsx ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Chart as ChartJS,
3
+ ArcElement,
4
+ Tooltip,
5
+ Legend,
6
+ CategoryScale,
7
+ LinearScale,
8
+ BarElement,
9
+ Title,
10
+ RadialLinearScale,
11
+ PointElement,
12
+ LineElement,
13
+ Filler,
14
+ } from 'chart.js';
15
+ import { Doughnut, Bar, Radar } from 'react-chartjs-2';
16
+
17
+ ChartJS.register(
18
+ ArcElement,
19
+ Tooltip,
20
+ Legend,
21
+ CategoryScale,
22
+ LinearScale,
23
+ BarElement,
24
+ Title,
25
+ RadialLinearScale,
26
+ PointElement,
27
+ LineElement,
28
+ Filler
29
+ );
30
+
31
+ export function DTIGauge({ dti }) {
32
+ const data = {
33
+ labels: ['DTI', 'Remaining Capacity'],
34
+ datasets: [
35
+ {
36
+ data: [dti, Math.max(0, 100 - dti)],
37
+ backgroundColor: [
38
+ dti > 50 ? '#ef4444' : dti > 30 ? '#f59e0b' : '#10b981',
39
+ '#f3f4f6'
40
+ ],
41
+ borderWidth: 0,
42
+ cutout: '85%'
43
+ }
44
+ ]
45
+ };
46
+
47
+ const options = {
48
+ rotation: -90,
49
+ circumference: 180,
50
+ plugins: {
51
+ legend: { display: false },
52
+ tooltip: { enabled: false }
53
+ },
54
+ maintainAspectRatio: true,
55
+ };
56
+
57
+ return (
58
+ <div style={{ position: 'relative', width: '220px', margin: '0 auto' }}>
59
+ <Doughnut data={data} options={options} />
60
+ <div style={{
61
+ position: 'absolute',
62
+ top: '70%',
63
+ left: '50%',
64
+ transform: 'translate(-50%, -50%)',
65
+ textAlign: 'center'
66
+ }}>
67
+ <div style={{ fontSize: '1.75rem', fontWeight: '700' }}>{dti.toFixed(1)}%</div>
68
+ <div style={{ fontSize: '0.75rem', color: '#6b7280', fontWeight: '600' }}>DEBT-TO-INCOME</div>
69
+ </div>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ export function ComparisonRadar({ userResults }) {
75
+ const benchmarks = JSON.parse(userResults.benchmarks_json || "{}");
76
+
77
+ // Clean keys for display
78
+ const labelMap = {
79
+ 'ApplicantIncome': 'Income',
80
+ 'LoanAmount': 'Loan Size',
81
+ 'Total_Income': 'Total Rev',
82
+ 'EMI': 'EMI Load',
83
+ 'Credit_History': 'Credit Score'
84
+ };
85
+
86
+ const keys = Object.keys(labelMap);
87
+
88
+ // Normalize values roughly for radar (0-100 scale)
89
+ const normalize = (val, max) => Math.min(100, (val / max) * 100);
90
+
91
+ const userData = [
92
+ normalize(userResults.applicant_income, benchmarks.ApplicantIncome * 2 || 10000),
93
+ normalize(userResults.loan_amount, benchmarks.LoanAmount * 2 || 500),
94
+ normalize(userResults.applicant_income + userResults.coapplicant_income, benchmarks.Total_Income * 2 || 15000),
95
+ normalize((userResults.loan_amount * 1000) / userResults.loan_amount_term, benchmarks.EMI * 2 || 2000),
96
+ userResults.credit_history * 100
97
+ ];
98
+
99
+ const benchData = [50, 50, 50, 50, 50]; // Benchmarks are normalized to center
100
+
101
+ const data = {
102
+ labels: keys.map(k => labelMap[k]),
103
+ datasets: [
104
+ {
105
+ label: 'Your Profile',
106
+ data: userData,
107
+ backgroundColor: 'rgba(242, 98, 47, 0.2)',
108
+ borderColor: '#f2622f',
109
+ borderWidth: 2,
110
+ pointBackgroundColor: '#f2622f',
111
+ },
112
+ {
113
+ label: 'Approved Average',
114
+ data: benchData,
115
+ backgroundColor: 'rgba(107, 114, 128, 0.1)',
116
+ borderColor: '#9ca3af',
117
+ borderWidth: 1,
118
+ borderDash: [5, 5],
119
+ pointRadius: 0
120
+ }
121
+ ]
122
+ };
123
+
124
+ const options = {
125
+ scales: {
126
+ r: {
127
+ angleLines: { display: false },
128
+ suggestedMin: 0,
129
+ suggestedMax: 100,
130
+ ticks: { display: false }
131
+ }
132
+ },
133
+ plugins: {
134
+ legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 10 } } }
135
+ }
136
+ };
137
+
138
+ return <Radar data={data} options={options} />;
139
+ }
140
+
141
+ export function FeatureImportanceBar({ featuresJson }) {
142
+ let features = {};
143
+ try {
144
+ features = JSON.parse(featuresJson);
145
+ } catch(e) {
146
+ return null;
147
+ }
148
+
149
+ const sorted = Object.entries(features)
150
+ .sort(([, a], [, b]) => b - a)
151
+ .slice(0, 5);
152
+
153
+ const data = {
154
+ labels: sorted.map(([k]) => k.replace(/_/g, ' ')),
155
+ datasets: [{
156
+ label: 'Impact Score',
157
+ data: sorted.map(([,v]) => v),
158
+ backgroundColor: '#f2622f',
159
+ borderRadius: 6
160
+ }]
161
+ };
162
+
163
+ const options = {
164
+ indexAxis: 'y',
165
+ plugins: { legend: { display: false } },
166
+ scales: {
167
+ x: { display: false },
168
+ y: { grid: { display: false }, ticks: { font: { size: 11 } } }
169
+ }
170
+ };
171
+
172
+ return <Bar data={data} options={options} />;
173
+ }
frontend/src/components/Dashboard.jsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RefreshCw, CheckCircle, XCircle, BrainCircuit, ShieldCheck, TrendingDown, Target, Lightbulb } from 'lucide-react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import { DTIGauge, FeatureImportanceBar, ComparisonRadar } from './Charts';
4
+
5
+ export default function Dashboard({ result, onReset }) {
6
+ const isApproved = result.prediction === 'Y';
7
+
8
+ // Parse structured AI points
9
+ const explanationPoints = result.explanation_text
10
+ ? result.explanation_text.split('@@POINT@@').filter(p => p.trim() !== '')
11
+ : [];
12
+
13
+ return (
14
+ <div className="animate-in">
15
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
16
+ <div className={`status-badge ${isApproved ? 'status-approved' : 'status-rejected'}`}>
17
+ {isApproved ? <CheckCircle size={24} /> : <XCircle size={24} />}
18
+ {isApproved ? 'Application Approved' : 'Action Required: Rejection'}
19
+ </div>
20
+ <button onClick={onReset} className="btn" style={{ background: 'white', color: 'var(--text-main)', border: '1px solid var(--border-color)', boxShadow: 'none' }}>
21
+ <RefreshCw size={16} /> New Assessment
22
+ </button>
23
+ </div>
24
+
25
+ {/* Top Metric Grid */}
26
+ <div className="metric-grid">
27
+ <div className="metric-tile">
28
+ <span className="metric-label">Approval Confidence</span>
29
+ <span className="metric-value" style={{ color: isApproved ? 'var(--success)' : 'var(--danger)' }}>
30
+ {(result.confidence * 100).toFixed(0)}%
31
+ </span>
32
+ </div>
33
+ <div className="metric-tile">
34
+ <span className="metric-label">DTI Ratio</span>
35
+ <span className="metric-value">{result.dti_ratio.toFixed(1)}%</span>
36
+ </div>
37
+ <div className="metric-tile">
38
+ <span className="metric-label">Credit Level</span>
39
+ <span className="metric-value">{result.credit_history === 1 ? 'High' : 'At-Risk'}</span>
40
+ </div>
41
+ </div>
42
+
43
+ <div className="glass-card" style={{ marginBottom: '2rem' }}>
44
+ <h2 style={{ marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.75rem', color: 'var(--primary)', fontWeight: '700' }}>
45
+ <BrainCircuit size={28} />
46
+ AI Advisor Narrative
47
+ </h2>
48
+ <p style={{ color: 'var(--text-muted)', marginBottom: '1.5rem', fontSize: '1rem' }}>
49
+ Structured multi-point analysis from the Mistral Reasoning Engine.
50
+ </p>
51
+
52
+ <div className="narrative-grid">
53
+ {explanationPoints.length > 0 ? (
54
+ explanationPoints.map((point, index) => (
55
+ <div key={index} className="narrative-sub-card">
56
+ <div style={{ display: 'flex', gap: '1rem' }}>
57
+ <div style={{ color: 'var(--primary)', marginTop: '0.2rem' }}><Lightbulb size={20} /></div>
58
+ <div className="narrative-content">
59
+ <ReactMarkdown>{point.trim()}</ReactMarkdown>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ ))
64
+ ) : (
65
+ <div className="narrative-sub-card" style={{ gridColumn: '1 / -1' }}>
66
+ <p>{result.explanation_text || "Gathering advisor narrative..."}</p>
67
+ </div>
68
+ )}
69
+ </div>
70
+
71
+ {!isApproved && result.optimized_suggestion && (
72
+ <div className="ai-advice-box">
73
+ <h3><Target size={20} /> Actionable Path to Approval</h3>
74
+ <p style={{ fontSize: '1.1rem', color: '#4b5563' }}>{result.optimized_suggestion}</p>
75
+ </div>
76
+ )}
77
+ </div>
78
+
79
+ <div className="dashboard-layout">
80
+ <div className="glass-card">
81
+ <h3 style={{ marginBottom: '2rem', display: 'flex', alignItems: 'center', gap: '0.5rem', borderBottom: '1px solid var(--border-color)', paddingBottom: '1rem' }}>
82
+ <ShieldCheck size={20} color="var(--success)" />
83
+ Peer Comparison Radar
84
+ </h3>
85
+ <div style={{ height: '300px' }}>
86
+ <ComparisonRadar userResults={result} />
87
+ </div>
88
+ </div>
89
+
90
+ <div className="glass-card">
91
+ <h3 style={{ marginBottom: '2rem', display: 'flex', alignItems: 'center', gap: '0.5rem', borderBottom: '1px solid var(--border-color)', paddingBottom: '1rem' }}>
92
+ <TrendingDown size={20} color="var(--primary)" />
93
+ Impact Vector Analysis
94
+ </h3>
95
+ <FeatureImportanceBar featuresJson={result.feature_importance_json} />
96
+ </div>
97
+ </div>
98
+
99
+ <div style={{ textAlign: 'center', marginTop: '3rem', opacity: 0.6 }}>
100
+ <DTIGauge dti={result.dti_ratio} />
101
+ </div>
102
+ </div>
103
+ );
104
+ }
frontend/src/components/Form.jsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import axios from 'axios';
3
+ import { ArrowRight } from 'lucide-react';
4
+
5
+ export default function Form({ setResult, setLoading }) {
6
+ const [formData, setFormData] = useState({
7
+ gender: 'Male',
8
+ married: 'No',
9
+ dependents: '0',
10
+ education: 'Graduate',
11
+ self_employed: 'No',
12
+ applicant_income: 5000,
13
+ coapplicant_income: 0,
14
+ loan_amount: 100,
15
+ loan_amount_term: 360,
16
+ credit_history: 1.0,
17
+ property_area: 'Urban'
18
+ });
19
+
20
+ const handleChange = (e) => {
21
+ const { name, value } = e.target;
22
+ setFormData(prev => ({
23
+ ...prev,
24
+ [name]: ['applicant_income', 'coapplicant_income', 'loan_amount', 'loan_amount_term', 'credit_history'].includes(name)
25
+ ? parseFloat(value)
26
+ : value
27
+ }));
28
+ };
29
+
30
+ const handleSubmit = async (e) => {
31
+ e.preventDefault();
32
+ setLoading(true);
33
+ try {
34
+ const response = await axios.post('http://localhost:8000/api/predict', formData);
35
+ setResult(response.data);
36
+ } catch (error) {
37
+ console.error("Prediction error", error);
38
+ alert('Error predicting. Make sure backend is running.');
39
+ } finally {
40
+ setLoading(false);
41
+ }
42
+ };
43
+
44
+ return (
45
+ <div className="glass-card" style={{ maxWidth: '800px', margin: '0 auto' }}>
46
+ <h2 style={{ marginBottom: '1.5rem' }}>Application Details</h2>
47
+ <form onSubmit={handleSubmit}>
48
+ <div className="form-grid">
49
+ <div className="form-group">
50
+ <label>Gender</label>
51
+ <select name="gender" value={formData.gender} onChange={handleChange}>
52
+ <option>Male</option>
53
+ <option>Female</option>
54
+ </select>
55
+ </div>
56
+ <div className="form-group">
57
+ <label>Married</label>
58
+ <select name="married" value={formData.married} onChange={handleChange}>
59
+ <option>No</option>
60
+ <option>Yes</option>
61
+ </select>
62
+ </div>
63
+ <div className="form-group">
64
+ <label>Dependents</label>
65
+ <select name="dependents" value={formData.dependents} onChange={handleChange}>
66
+ <option>0</option>
67
+ <option>1</option>
68
+ <option>2</option>
69
+ <option>3+</option>
70
+ </select>
71
+ </div>
72
+ <div className="form-group">
73
+ <label>Education</label>
74
+ <select name="education" value={formData.education} onChange={handleChange}>
75
+ <option>Graduate</option>
76
+ <option>Not Graduate</option>
77
+ </select>
78
+ </div>
79
+ <div className="form-group">
80
+ <label>Self Employed</label>
81
+ <select name="self_employed" value={formData.self_employed} onChange={handleChange}>
82
+ <option>No</option>
83
+ <option>Yes</option>
84
+ </select>
85
+ </div>
86
+ <div className="form-group">
87
+ <label>Applicant Income (Monthly)</label>
88
+ <input type="number" name="applicant_income" value={formData.applicant_income} onChange={handleChange} required />
89
+ </div>
90
+ <div className="form-group">
91
+ <label>Co-Applicant Income</label>
92
+ <input type="number" name="coapplicant_income" value={formData.coapplicant_income} onChange={handleChange} required />
93
+ </div>
94
+ <div className="form-group">
95
+ <label>Loan Amount (in Thousands)</label>
96
+ <input type="number" name="loan_amount" value={formData.loan_amount} onChange={handleChange} required />
97
+ </div>
98
+ <div className="form-group">
99
+ <label>Loan Term (Months)</label>
100
+ <input type="number" name="loan_amount_term" value={formData.loan_amount_term} onChange={handleChange} required />
101
+ </div>
102
+ <div className="form-group">
103
+ <label>Credit History</label>
104
+ <select name="credit_history" value={formData.credit_history} onChange={handleChange}>
105
+ <option value="1.0">Good (1.0)</option>
106
+ <option value="0.0">Bad (0.0)</option>
107
+ </select>
108
+ </div>
109
+ <div className="form-group">
110
+ <label>Property Area</label>
111
+ <select name="property_area" value={formData.property_area} onChange={handleChange}>
112
+ <option>Urban</option>
113
+ <option>Semiurban</option>
114
+ <option>Rural</option>
115
+ </select>
116
+ </div>
117
+ </div>
118
+
119
+ <div style={{ marginTop: '2rem', textAlign: 'right' }}>
120
+ <button type="submit" className="btn">
121
+ Analyze Application <ArrowRight size={18} />
122
+ </button>
123
+ </div>
124
+ </form>
125
+ </div>
126
+ );
127
+ }
frontend/src/components/Home.jsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ArrowRight, BrainCircuit, LineChart, ShieldCheck, Database, Zap, FileSpreadsheet } from 'lucide-react';
2
+
3
+ export default function Home({ onProceed }) {
4
+ return (
5
+ <div className="home-container" style={{ padding: '2rem 0' }}>
6
+ {/* Hero Section */}
7
+ <div style={{ textAlign: 'center', marginBottom: '5rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
8
+ <div style={{ background: 'var(--primary-glow)', padding: '1.5rem', borderRadius: '50%', color: 'var(--primary)', marginBottom: '2rem' }}>
9
+ <BrainCircuit size={64} />
10
+ </div>
11
+
12
+ <h1 style={{ fontSize: '4.5rem', fontWeight: '800', letterSpacing: '-0.04em', marginBottom: '1.5rem', color: 'var(--text-main)', lineHeight: '1.1' }}>
13
+ Next-Generation <br/>
14
+ <span style={{ color: 'var(--primary)' }}>Loan Intelligence</span>
15
+ </h1>
16
+
17
+ <p style={{ fontSize: '1.25rem', color: 'var(--text-muted)', maxWidth: '650px', marginBottom: '3rem', lineHeight: '1.6' }}>
18
+ Evaluate loan applications instantly. Our hybrid architecture combines deterministic mathematical modeling with Mistral Large 3 AI to provide human-readable, actionable financial decisions.
19
+ </p>
20
+
21
+ <button onClick={onProceed} className="btn" style={{ fontSize: '1.15rem', padding: '1.2rem 3.5rem', borderRadius: '40px' }}>
22
+ Proceed to Application <ArrowRight size={22} />
23
+ </button>
24
+ </div>
25
+
26
+ {/* Feature Grid Section */}
27
+ <div style={{ marginBottom: '4rem' }}>
28
+ <h2 style={{ textAlign: 'center', fontSize: '2.5rem', marginBottom: '3rem', fontWeight: '700' }}>Platform Capabilities</h2>
29
+
30
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '2rem' }}>
31
+ {/* Card 1 */}
32
+ <div className="glass-card" style={{ padding: '2rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
33
+ <div style={{ background: '#ecfdf5', width: '50px', height: '50px', borderRadius: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--success)' }}>
34
+ <ShieldCheck size={28} />
35
+ </div>
36
+ <h3 style={{ fontSize: '1.5rem', fontWeight: '700' }}>Deterministic Engine</h3>
37
+ <p style={{ color: 'var(--text-muted)', lineHeight: '1.6' }}>
38
+ Base evaluations are driven by a strictly enforced Random Forest pipeline. No hallucinations—just pure math calculating Debt-to-Income ratios and credit health.
39
+ </p>
40
+ </div>
41
+
42
+ {/* Card 2 */}
43
+ <div className="glass-card" style={{ padding: '2rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
44
+ <div style={{ background: 'var(--primary-glow)', width: '50px', height: '50px', borderRadius: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--primary)' }}>
45
+ <Zap size={28} />
46
+ </div>
47
+ <h3 style={{ fontSize: '1.5rem', fontWeight: '700' }}>Generative Explanations</h3>
48
+ <p style={{ color: 'var(--text-muted)', lineHeight: '1.6' }}>
49
+ We pipe the mathematical packet into NVIDIA NIM (Mistral 3). It translates complex metrics into a multi-point, easy-to-read narrative for applicants.
50
+ </p>
51
+ </div>
52
+
53
+ {/* Card 3 */}
54
+ <div className="glass-card" style={{ padding: '2rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
55
+ <div style={{ background: '#eff6ff', width: '50px', height: '50px', borderRadius: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#3b82f6' }}>
56
+ <LineChart size={28} />
57
+ </div>
58
+ <h3 style={{ fontSize: '1.5rem', fontWeight: '700' }}>Visual Benchmarking</h3>
59
+ <p style={{ color: 'var(--text-muted)', lineHeight: '1.6' }}>
60
+ Dynamically compare incoming applications against a "Typical Approved Profile" using our built-in Radar comparisons and internal ML weigh distribution charts.
61
+ </p>
62
+ </div>
63
+ </div>
64
+ </div>
65
+
66
+ {/* Footer Banner */}
67
+ <div style={{ borderRadius: '20px', background: 'linear-gradient(135deg, #1a1b1e 0%, #374151 100%)', color: 'white', padding: '3rem', textAlign: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1.5rem' }}>
68
+ <Database size={40} color="var(--primary)" />
69
+ <h2 style={{ fontSize: '2rem' }}>Ready to analyze your next portfolio?</h2>
70
+ <button onClick={onProceed} className="btn" style={{ background: 'white', color: 'var(--text-main)', boxShadow: 'none' }}>
71
+ Start Prediction Pipeline
72
+ </button>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
frontend/src/components/Sidebar.jsx ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import axios from 'axios';
3
+ import { Plus, Clock, CheckCircle2, XCircle, ChevronRight, Trash2 } from 'lucide-react';
4
+
5
+ export default function Sidebar({ onSelectResult, onNewAssessment }) {
6
+ const [history, setHistory] = useState([]);
7
+ const [loading, setLoading] = useState(true);
8
+
9
+ const fetchHistory = async () => {
10
+ try {
11
+ setLoading(true);
12
+ const res = await axios.get('http://localhost:8000/api/history?limit=20');
13
+ setHistory(res.data);
14
+ } catch (error) {
15
+ console.error("Failed to load history", error);
16
+ } finally {
17
+ setLoading(false);
18
+ }
19
+ };
20
+
21
+ const handleClearHistory = async () => {
22
+ if (window.confirm("Are you sure you want to clear all history? This action cannot be undone.")) {
23
+ try {
24
+ await axios.delete('http://localhost:8000/api/history');
25
+ setHistory([]);
26
+ } catch (error) {
27
+ console.error("Failed to clear history", error);
28
+ alert("Could not clear history");
29
+ }
30
+ }
31
+ };
32
+
33
+ useEffect(() => {
34
+ fetchHistory();
35
+ }, []);
36
+
37
+ return (
38
+ <div className="sidebar">
39
+ <button
40
+ className="btn"
41
+ onClick={onNewAssessment}
42
+ style={{ width: '100%', marginBottom: '1.5rem', justifyContent: 'center' }}
43
+ >
44
+ <Plus size={18} /> New Assessment
45
+ </button>
46
+
47
+ <div style={{ fontSize: '0.85rem', fontWeight: '600', color: 'var(--text-muted)', marginBottom: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
48
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
49
+ <Clock size={14} /> RECENT ASSESSMENTS
50
+ </div>
51
+ {history.length > 0 && (
52
+ <button
53
+ onClick={handleClearHistory}
54
+ style={{ background: 'none', border: 'none', color: 'var(--danger)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.2rem', fontSize: '0.75rem', fontWeight: '600' }}
55
+ title="Clear All History"
56
+ >
57
+ <Trash2 size={14} />
58
+ Clear
59
+ </button>
60
+ )}
61
+ </div>
62
+
63
+ <div className="history-list">
64
+ {loading ? (
65
+ <div style={{ padding: '1rem', textAlign: 'center', color: 'var(--text-muted)' }}>Loading...</div>
66
+ ) : history.length === 0 ? (
67
+ <div style={{ padding: '1rem', textAlign: 'center', color: 'var(--text-muted)', fontSize: '0.9rem' }}>No history found.</div>
68
+ ) : (
69
+ history.map((item) => (
70
+ <div
71
+ key={item.id}
72
+ className="history-item"
73
+ onClick={() => onSelectResult(item)}
74
+ >
75
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
76
+ {item.prediction === 'Y' ? (
77
+ <CheckCircle2 size={16} color="var(--success)" />
78
+ ) : (
79
+ <XCircle size={16} color="var(--danger)" />
80
+ )}
81
+ <span style={{ fontWeight: '600', fontSize: '0.95rem' }}>
82
+ {item.prediction === 'Y' ? 'Approved' : 'Rejected'}
83
+ </span>
84
+ </div>
85
+ <div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', display: 'flex', justifyContent: 'space-between' }}>
86
+ <span>Income: ${item.applicant_income}</span>
87
+ <ChevronRight size={14} />
88
+ </div>
89
+ </div>
90
+ ))
91
+ )}
92
+ </div>
93
+ </div>
94
+ );
95
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');
2
+
3
+ :root {
4
+ --bg-color: #fcfbf9;
5
+ --text-main: #1a1b1e;
6
+ --text-muted: #6b7280;
7
+ --primary: #f2622f;
8
+ --primary-glow: rgba(242, 98, 47, 0.15);
9
+ --card-bg: #ffffff;
10
+ --border-color: #efedeb;
11
+ --success: #10b981;
12
+ --danger: #ef4444;
13
+ --warning: #f59e0b;
14
+ --light-bg: #fff9f5;
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ margin: 0;
20
+ padding: 0;
21
+ }
22
+
23
+ body {
24
+ font-family: 'Outfit', sans-serif;
25
+ background-color: var(--bg-color);
26
+ color: var(--text-main);
27
+ line-height: 1.6;
28
+ -webkit-font-smoothing: antialiased;
29
+ }
30
+
31
+ .app-layout {
32
+ display: flex;
33
+ height: 100vh;
34
+ width: 100vw;
35
+ overflow: hidden;
36
+ background-color: var(--bg-color);
37
+ }
38
+
39
+ .sidebar {
40
+ width: 280px;
41
+ background-color: #fcfbf9; /* Matching current theme */
42
+ border-right: 1px solid var(--border-color);
43
+ display: flex;
44
+ flex-direction: column;
45
+ padding: 1.5rem;
46
+ overflow-y: auto;
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ .main-content {
51
+ flex: 1;
52
+ overflow-y: auto;
53
+ padding: 3rem 2rem;
54
+ background-color: #ffffff;
55
+ }
56
+
57
+ .content-container {
58
+ max-width: 1100px;
59
+ margin: 0 auto;
60
+ }
61
+
62
+ .history-list {
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: 0.75rem;
66
+ }
67
+
68
+ .history-item {
69
+ background: #ffffff;
70
+ border: 1px solid var(--border-color);
71
+ padding: 1rem;
72
+ border-radius: 12px;
73
+ cursor: pointer;
74
+ transition: all 0.2s;
75
+ }
76
+
77
+ .history-item:hover {
78
+ border-color: var(--primary);
79
+ transform: translateY(-2px);
80
+ box-shadow: 0 4px 8px rgba(0,0,0,0.03);
81
+ }
82
+
83
+ .back-to-home-btn {
84
+ position: absolute;
85
+ top: 1.5rem;
86
+ right: 2rem;
87
+ background: white;
88
+ border: 1px solid var(--border-color);
89
+ color: var(--text-main);
90
+ padding: 0.6rem 1.2rem;
91
+ border-radius: 30px;
92
+ font-size: 0.9rem;
93
+ font-weight: 600;
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 0.5rem;
97
+ cursor: pointer;
98
+ z-index: 100;
99
+ transition: all 0.2s;
100
+ box-shadow: 0 4px 12px rgba(0,0,0,0.03);
101
+ }
102
+
103
+ .back-to-home-btn:hover {
104
+ border-color: var(--primary);
105
+ color: var(--primary);
106
+ transform: translateY(-1px);
107
+ box-shadow: 0 6px 16px rgba(0,0,0,0.06);
108
+ }
109
+
110
+ .header {
111
+ text-align: center;
112
+ margin-bottom: 4rem;
113
+ }
114
+
115
+ .header h1 {
116
+ font-size: 3.5rem;
117
+ font-weight: 700;
118
+ letter-spacing: -0.03em;
119
+ margin-bottom: 0.75rem;
120
+ background: linear-gradient(135deg, #1a1b1e 0%, #4b5563 100%);
121
+ -webkit-background-clip: text;
122
+ -webkit-text-fill-color: transparent;
123
+ }
124
+
125
+ .header p {
126
+ color: var(--text-muted);
127
+ font-size: 1.2rem;
128
+ }
129
+
130
+ .glass-card {
131
+ background: var(--card-bg);
132
+ border: 1px solid var(--border-color);
133
+ border-radius: 20px;
134
+ padding: 2.5rem;
135
+ box-shadow: 0 10px 30px -10px rgba(0,0,0,0.05);
136
+ }
137
+
138
+ .metric-grid {
139
+ display: grid;
140
+ grid-template-columns: repeat(3, 1fr);
141
+ gap: 1.5rem;
142
+ margin-bottom: 2rem;
143
+ }
144
+
145
+ .metric-tile {
146
+ background: white;
147
+ border: 1px solid var(--border-color);
148
+ padding: 1.5rem;
149
+ border-radius: 16px;
150
+ text-align: center;
151
+ }
152
+
153
+ .metric-value {
154
+ font-size: 1.75rem;
155
+ font-weight: 700;
156
+ display: block;
157
+ }
158
+
159
+ .metric-label {
160
+ font-size: 0.85rem;
161
+ color: var(--text-muted);
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.05em;
164
+ font-weight: 600;
165
+ }
166
+
167
+ /* NARRATIVE SUB-CARDS */
168
+ .narrative-grid {
169
+ display: grid;
170
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
171
+ gap: 1.25rem;
172
+ margin-top: 1.5rem;
173
+ }
174
+
175
+ .narrative-sub-card {
176
+ background: white;
177
+ border: 1px solid var(--border-color);
178
+ padding: 1.5rem;
179
+ border-radius: 16px;
180
+ border-left: 5px solid var(--primary);
181
+ box-shadow: 0 2px 8px rgba(0,0,0,0.02);
182
+ transition: all 0.2s ease;
183
+ }
184
+
185
+ .narrative-sub-card:hover {
186
+ transform: translateY(-2px);
187
+ box-shadow: 0 10px 20px rgba(0,0,0,0.04);
188
+ }
189
+
190
+ .btn {
191
+ background-color: var(--primary);
192
+ color: white;
193
+ border: none;
194
+ padding: 0.85rem 2rem;
195
+ font-size: 1rem;
196
+ font-weight: 600;
197
+ border-radius: 30px;
198
+ cursor: pointer;
199
+ transition: all 0.3s;
200
+ display: inline-flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ gap: 0.6rem;
204
+ }
205
+
206
+ .btn:hover {
207
+ background-color: #df5320;
208
+ transform: translateY(-2px);
209
+ }
210
+
211
+ .form-grid {
212
+ display: grid;
213
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
214
+ gap: 2rem;
215
+ }
216
+
217
+ .form-group {
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: 0.6rem;
221
+ }
222
+
223
+ .form-group label {
224
+ font-weight: 600;
225
+ font-size: 0.95rem;
226
+ }
227
+
228
+ .form-group input,
229
+ .form-group select {
230
+ padding: 0.9rem;
231
+ border: 1px solid var(--border-color);
232
+ border-radius: 12px;
233
+ font-family: inherit;
234
+ font-size: 1rem;
235
+ background-color: #fdfdfd;
236
+ }
237
+
238
+ .dashboard-layout {
239
+ display: grid;
240
+ grid-template-columns: 1.2fr 1fr;
241
+ gap: 2rem;
242
+ margin-top: 2rem;
243
+ }
244
+
245
+ @media (max-width: 900px) {
246
+ .metric-grid { grid-template-columns: 1fr; }
247
+ .dashboard-layout { grid-template-columns: 1fr; }
248
+ }
249
+
250
+ .status-badge {
251
+ padding: 0.5rem 1.25rem;
252
+ border-radius: 30px;
253
+ font-weight: 700;
254
+ font-size: 1rem;
255
+ display: inline-flex;
256
+ align-items: center;
257
+ gap: 0.6rem;
258
+ margin-bottom: 2rem;
259
+ }
260
+
261
+ .status-approved { background-color: #ecfdf5; color: var(--success); }
262
+ .status-rejected { background-color: #fef2f2; color: var(--danger); }
263
+
264
+ .ai-advice-box {
265
+ background-color: #fffaf8;
266
+ border: 1px solid #fee2d5;
267
+ border-left: 6px solid var(--primary);
268
+ padding: 2rem;
269
+ border-radius: 16px;
270
+ margin-top: 2rem;
271
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
pyproject.toml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "loan-prediction-system"
7
+ version = "0.1.0"
8
+ description = "Enterprise Loan AI Dashboard with Deterministic ML and LLM Advisor"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ authors = [
12
+ { name = "Siddharaj Shirke" },
13
+ ]
14
+ license = { text = "MIT" }
15
+ dependencies = [
16
+ "fastapi>=0.100.0",
17
+ "uvicorn>=0.23.0",
18
+ "pydantic>=2.0.0",
19
+ "sqlalchemy>=2.0.0",
20
+ "openai>=1.0.0",
21
+ "scikit-learn>=1.2.0",
22
+ "pandas>=2.0.0",
23
+ "numpy>=1.24.0",
24
+ "python-dotenv>=1.0.0",
25
+ "huggingface_hub>=0.19.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ loan-app = "uvicorn backend.main:app --port 8000 --reload"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["backend"]
scratch/reset_db.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from backend.db.database import engine
3
+ from backend.db import models
4
+
5
+ def reset_db():
6
+ print("Attempting to reset database...")
7
+ try:
8
+ # Drop all tables
9
+ models.Base.metadata.drop_all(bind=engine)
10
+ print("Dropped all tables.")
11
+
12
+ # Recreate all tables
13
+ models.Base.metadata.create_all(bind=engine)
14
+ print("Recreated all tables with new schema.")
15
+
16
+ # Alternatively, try to delete the file if it exists
17
+ db_path = "loan_system.db"
18
+ if os.path.exists(db_path):
19
+ try:
20
+ os.remove(db_path)
21
+ print(f"Deleted {db_path} file.")
22
+ except Exception as e:
23
+ print(f"Could not delete file (might be locked): {e}")
24
+ except Exception as e:
25
+ print(f"Error during reset: {e}")
26
+
27
+ if __name__ == "__main__":
28
+ reset_db()