Spaces:
Sleeping
Sleeping
Siddharaj Shirke commited on
Commit ·
67c8aca
1
Parent(s): cc728a5
v1 completed successfully
Browse files- .dockerignore +15 -0
- .env.example +11 -0
- .gitignore +26 -0
- Dockerfile +45 -0
- README.md +102 -29
- assets/about.png +0 -0
- assets/home.png +0 -0
- assets/predict.png +0 -0
- backend/db/database.py +29 -0
- backend/db/models.py +35 -0
- backend/db/schemas.py +34 -0
- backend/logic/deterministic.py +109 -0
- backend/logic/ml_model.py +106 -0
- backend/main.py +98 -0
- backend/requirements.txt +10 -0
- backend/services/hf_sync.py +49 -0
- backend/services/llm_advisor.py +94 -0
- backend/tests/test_llm_fallback.py +77 -0
- frontend/.gitignore +24 -0
- frontend/README.md +16 -0
- frontend/eslint.config.js +29 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +32 -0
- frontend/public/favicon.svg +1 -0
- frontend/public/icons.svg +24 -0
- frontend/src/App.css +184 -0
- frontend/src/App.jsx +72 -0
- frontend/src/assets/hero.png +0 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/assets/vite.svg +1 -0
- frontend/src/components/Charts.jsx +173 -0
- frontend/src/components/Dashboard.jsx +104 -0
- frontend/src/components/Form.jsx +127 -0
- frontend/src/components/Home.jsx +76 -0
- frontend/src/components/Sidebar.jsx +95 -0
- frontend/src/index.css +271 -0
- frontend/src/main.jsx +10 -0
- frontend/vite.config.js +7 -0
- pyproject.toml +32 -0
- scratch/reset_db.py +28 -0
.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
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
---
|
| 16 |
|
| 17 |
-
##
|
| 18 |
|
| 19 |
-
*
|
| 20 |
-
* Streamlit
|
| 21 |
-
* Pandas
|
| 22 |
-
* NumPy
|
| 23 |
-
* Scikit-learn
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
#
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
4. Result is displayed as Approved or Rejected with confidence
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
#
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
*
|
| 39 |
-
*
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
---
|
| 44 |
|
| 45 |
-
##
|
| 46 |
|
| 47 |
-
This project
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
---
|
| 50 |
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|