Spaces:
Running
Running
Upload 2 files
Browse files
app.py
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, re, json, time, pickle, random, traceback
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import gradio as gr
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
import requests as req
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Tuple
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
from huggingface_hub import InferenceClient
|
| 12 |
+
except Exception:
|
| 13 |
+
InferenceClient = None
|
| 14 |
+
|
| 15 |
+
# =========================================================
|
| 16 |
+
# CONFIG
|
| 17 |
+
# =========================================================
|
| 18 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 19 |
+
HF_API_KEY = os.environ.get("HF_API_KEY", "").strip()
|
| 20 |
+
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-ai/DeepSeek-R1").strip()
|
| 21 |
+
HF_PROVIDER = os.environ.get("HF_PROVIDER", "novita").strip()
|
| 22 |
+
N8N_SALARY_PREDICTION_URL = os.environ.get("N8N_SALARY_PREDICTION_URL", "").strip()
|
| 23 |
+
N8N_HR_FEEDBACK_URL = os.environ.get("N8N_HR_FEEDBACK_URL", "").strip()
|
| 24 |
+
N8N_ANOMALY_DETECTION_URL = os.environ.get("N8N_ANOMALY_DETECTION_URL", "").strip()
|
| 25 |
+
LLM_ENABLED = bool(HF_API_KEY) and InferenceClient is not None
|
| 26 |
+
llm_client = (InferenceClient(provider=HF_PROVIDER, api_key=HF_API_KEY) if LLM_ENABLED else None)
|
| 27 |
+
|
| 28 |
+
P = ["#45FFCA","#D09CFA","#FF9B9B","#F875AA","#3EDBF0","#F2C637","#7c5cbf","#2ec4a0"]
|
| 29 |
+
GOLD = "#F2C637"
|
| 30 |
+
PURPLE = "#28096d"
|
| 31 |
+
|
| 32 |
+
# =========================================================
|
| 33 |
+
# DATA
|
| 34 |
+
# =========================================================
|
| 35 |
+
_CACHE = {}
|
| 36 |
+
|
| 37 |
+
def _load_csv(f):
|
| 38 |
+
p = BASE_DIR / f
|
| 39 |
+
if p.exists():
|
| 40 |
+
try: return pd.read_csv(p)
|
| 41 |
+
except: pass
|
| 42 |
+
return pd.DataFrame()
|
| 43 |
+
|
| 44 |
+
def _gen_base(n=500):
|
| 45 |
+
rng = np.random.default_rng(42)
|
| 46 |
+
edu_levels = ["Bachelor's","Master's","PhD"]
|
| 47 |
+
job_pool = {
|
| 48 |
+
"Bachelor's":["Data Analyst","Junior Engineer","Business Analyst","HR Specialist","Marketing Manager"],
|
| 49 |
+
"Master's":["Data Scientist","Senior Engineer","Product Manager","Financial Analyst","ML Engineer"],
|
| 50 |
+
"PhD":["Director of Analytics","Research Scientist","VP of Engineering","Chief Data Officer","AI Researcher"],
|
| 51 |
+
}
|
| 52 |
+
rows = []
|
| 53 |
+
for _ in range(n):
|
| 54 |
+
edu = rng.choice(edu_levels, p=[0.55,0.35,0.10])
|
| 55 |
+
exp = int(rng.integers(0,35))
|
| 56 |
+
age = int(np.clip(22+exp+rng.integers(-2,5),22,65))
|
| 57 |
+
gender = rng.choice(["Male","Female"], p=[0.52,0.48])
|
| 58 |
+
job = rng.choice(job_pool[edu])
|
| 59 |
+
base = {"Bachelor's":52000,"Master's":68000,"PhD":88000}[edu]
|
| 60 |
+
sal = base + exp*3100 + (age-22)*180 + float(rng.normal(0,6000))
|
| 61 |
+
if gender=="Male": sal *= float(rng.uniform(1.0,1.06))
|
| 62 |
+
sal = max(25000,int(sal))
|
| 63 |
+
rows.append({"Age":age,"Gender":gender,"Education Level":edu,"Job Title":job,"Years of Experience":exp,"Salary":sal})
|
| 64 |
+
df = pd.DataFrame(rows)
|
| 65 |
+
def tier(r):
|
| 66 |
+
s={"Bachelor's":1,"Master's":2,"PhD":3}.get(r["Education Level"],1)
|
| 67 |
+
if r["Years of Experience"]>=15 and s>=2: return "senior"
|
| 68 |
+
elif r["Years of Experience"]>=7 or s==3: return "mid"
|
| 69 |
+
return "junior"
|
| 70 |
+
df["career_tier"] = df.apply(tier,axis=1)
|
| 71 |
+
df["salary_growth"] = df.apply(lambda r: int(r["Salary"]*(0.15 if r["career_tier"]=="senior" else 0.30 if r["career_tier"]=="mid" else 0.50)+float(np.random.normal(0,2000))),axis=1)
|
| 72 |
+
return df
|
| 73 |
+
|
| 74 |
+
def _gen_progression(base_df):
|
| 75 |
+
rows=[]
|
| 76 |
+
for _,row in base_df.iterrows():
|
| 77 |
+
t=row["career_tier"]; base=row["Salary"]
|
| 78 |
+
growth=np.linspace(0.85 if t=="senior" else 0.70 if t=="mid" else 0.50,1.0,5)
|
| 79 |
+
for i,yr in enumerate(range(2020,2025)):
|
| 80 |
+
sal=int(np.clip(base*(growth[i]+np.random.normal(0,0.03)),20000,None))
|
| 81 |
+
rows.append({"career_tier":t,"year":yr,"salary_that_year":sal})
|
| 82 |
+
return pd.DataFrame(rows)
|
| 83 |
+
|
| 84 |
+
def _gen_feedback(base_df):
|
| 85 |
+
pools={"senior":["Consistently exceeds expectations.","Key leader and mentor.","Delivers high-impact results.","Trusted advisor to management."],"mid":["Solid contributor, meets targets.","Shows initiative and grows.","Dependable team member.","Takes ownership of projects."],"junior":["Eager learner, developing skills.","Shows promise under coaching.","Making good progress.","Improving steadily with support."]}
|
| 86 |
+
rows=[]
|
| 87 |
+
for _,row in base_df.iterrows():
|
| 88 |
+
for comment in random.sample(pools[row["career_tier"]],2):
|
| 89 |
+
rows.append({"career_tier":row["career_tier"],"feedback_comment":comment,"Salary":row["Salary"]})
|
| 90 |
+
return pd.DataFrame(rows)
|
| 91 |
+
|
| 92 |
+
def get_base_df():
|
| 93 |
+
if "base" not in _CACHE:
|
| 94 |
+
df=_load_csv("employee_analysis_ready.csv")
|
| 95 |
+
if df.empty: df=_load_csv("Salary_Data.csv")
|
| 96 |
+
if df.empty: df=_gen_base()
|
| 97 |
+
if "career_tier" not in df.columns:
|
| 98 |
+
def tier(r):
|
| 99 |
+
s={"Bachelor's":1,"Master's":2,"PhD":3}.get(r.get("Education Level",""),1)
|
| 100 |
+
e=r.get("Years of Experience",0)
|
| 101 |
+
if e>=15 and s>=2: return "senior"
|
| 102 |
+
elif e>=7 or s==3: return "mid"
|
| 103 |
+
return "junior"
|
| 104 |
+
df["career_tier"]=df.apply(tier,axis=1)
|
| 105 |
+
_CACHE["base"]=df
|
| 106 |
+
return _CACHE["base"]
|
| 107 |
+
|
| 108 |
+
def get_prog_df():
|
| 109 |
+
if "prog" not in _CACHE:
|
| 110 |
+
df=_load_csv("synthetic_salary_progression.csv")
|
| 111 |
+
if df.empty: df=_gen_progression(get_base_df())
|
| 112 |
+
_CACHE["prog"]=df
|
| 113 |
+
return _CACHE["prog"]
|
| 114 |
+
|
| 115 |
+
def get_feed_df():
|
| 116 |
+
if "feed" not in _CACHE:
|
| 117 |
+
df=_load_csv("synthetic_employee_feedback.csv")
|
| 118 |
+
if df.empty: df=_gen_feedback(get_base_df())
|
| 119 |
+
_CACHE["feed"]=df
|
| 120 |
+
return _CACHE["feed"]
|
| 121 |
+
|
| 122 |
+
# =========================================================
|
| 123 |
+
# HELPERS
|
| 124 |
+
# =========================================================
|
| 125 |
+
def get_tier(age,exp,edu):
|
| 126 |
+
s={"Bachelor's":1,"Master's":2,"PhD":3}.get(edu,1)
|
| 127 |
+
if exp>=15 and s>=2: return "senior"
|
| 128 |
+
elif exp>=7 or s==3: return "mid"
|
| 129 |
+
return "junior"
|
| 130 |
+
|
| 131 |
+
def get_exp_group(exp):
|
| 132 |
+
if exp<=5: return "0-5 years"
|
| 133 |
+
elif exp<=10: return "6-10 years"
|
| 134 |
+
elif exp<=15: return "11-15 years"
|
| 135 |
+
elif exp<=20: return "16-20 years"
|
| 136 |
+
return "20+"
|
| 137 |
+
|
| 138 |
+
def _layout(**kw):
|
| 139 |
+
d=dict(template="plotly_white",paper_bgcolor="rgba(255,255,255,0.97)",plot_bgcolor="rgba(255,255,255,0.99)",font=dict(family="system-ui,sans-serif",color="#1a0a3d",size=12),margin=dict(l=50,r=20,t=55,b=45),legend=dict(orientation="h",yanchor="bottom",y=1.02,xanchor="right",x=1,bgcolor="rgba(255,255,255,0.9)",bordercolor="rgba(40,9,109,0.15)",borderwidth=1),title=dict(font=dict(size=14,color="#28096d")))
|
| 140 |
+
d.update(kw)
|
| 141 |
+
return d
|
| 142 |
+
|
| 143 |
+
def _n8n(url,payload):
|
| 144 |
+
if not url: return None
|
| 145 |
+
try:
|
| 146 |
+
r=req.post(url,json=payload,timeout=15)
|
| 147 |
+
return r.json()
|
| 148 |
+
except Exception as e:
|
| 149 |
+
return {"error":str(e)}
|
| 150 |
+
|
| 151 |
+
# =========================================================
|
| 152 |
+
# PREDICTION MODEL
|
| 153 |
+
# =========================================================
|
| 154 |
+
def _load_model():
|
| 155 |
+
for mp in [BASE_DIR/"rf_model.pkl",BASE_DIR/"model"/"rf_model.pkl"]:
|
| 156 |
+
cp=mp.parent/"train_columns.pkl"
|
| 157 |
+
if mp.exists() and cp.exists():
|
| 158 |
+
try:
|
| 159 |
+
with open(mp,"rb") as f: m=pickle.load(f)
|
| 160 |
+
with open(cp,"rb") as f: c=pickle.load(f)
|
| 161 |
+
return m,c
|
| 162 |
+
except: pass
|
| 163 |
+
return None,None
|
| 164 |
+
|
| 165 |
+
RF_MODEL,TRAIN_COLS=_load_model()
|
| 166 |
+
|
| 167 |
+
def predict_local(age,exp,edu,gender):
|
| 168 |
+
tier=get_tier(age,exp,edu)
|
| 169 |
+
exp_group=get_exp_group(exp)
|
| 170 |
+
if RF_MODEL and TRAIN_COLS:
|
| 171 |
+
row={"Age":age,"Years of Experience":exp,"Education Level_Master's":1 if edu=="Master's" else 0,"Education Level_PhD":1 if edu=="PhD" else 0,"salary_growth":exp*3500,"vader_score":0.3 if tier=="senior" else 0.15 if tier=="mid" else 0.05}
|
| 172 |
+
inp=pd.DataFrame([row])
|
| 173 |
+
tmp=pd.DataFrame([{"Experience Group":exp_group,"career_tier":tier}])
|
| 174 |
+
inp=pd.concat([inp,tmp],axis=1)
|
| 175 |
+
inp=pd.get_dummies(inp,columns=["Experience Group","career_tier"],drop_first=True)*1
|
| 176 |
+
for col in TRAIN_COLS:
|
| 177 |
+
if col not in inp.columns: inp[col]=0
|
| 178 |
+
inp=inp[TRAIN_COLS]
|
| 179 |
+
predicted=float(RF_MODEL.predict(inp)[0])
|
| 180 |
+
source="Random Forest Model"
|
| 181 |
+
else:
|
| 182 |
+
base={"Bachelor's":55000,"Master's":72000,"PhD":92000}.get(edu,55000)
|
| 183 |
+
predicted=base+(exp*3200)+((age-22)*250)
|
| 184 |
+
predicted=max(28000,predicted)
|
| 185 |
+
source="Formula Estimate"
|
| 186 |
+
predicted+=predicted*random.uniform(-0.01,0.01)
|
| 187 |
+
return {"predicted":round(predicted),"low":round(predicted*0.92),"high":round(predicted*1.08),"tier":tier,"exp_group":exp_group,"source":source}
|
| 188 |
+
|
| 189 |
+
def project_salary(base_salary, exp, edu, tier, years=5):
|
| 190 |
+
"""Project salary for next N years with promotion probability"""
|
| 191 |
+
projections = []
|
| 192 |
+
current = base_salary
|
| 193 |
+
current_exp = exp
|
| 194 |
+
for yr in range(1, years+1):
|
| 195 |
+
current_exp += 1
|
| 196 |
+
growth_rate = {"senior": 0.04, "mid": 0.06, "junior": 0.09}.get(tier, 0.05)
|
| 197 |
+
# Promotion bump every 3 years
|
| 198 |
+
promotion_bump = 0
|
| 199 |
+
if yr % 3 == 0:
|
| 200 |
+
promotion_bump = {"junior": 0.12, "mid": 0.10, "senior": 0.07}.get(tier, 0.08)
|
| 201 |
+
if yr % 3 == 0 and tier == "junior" and current_exp >= 7:
|
| 202 |
+
tier = "mid"
|
| 203 |
+
elif tier == "mid" and current_exp >= 15:
|
| 204 |
+
tier = "senior"
|
| 205 |
+
current = current * (1 + growth_rate + promotion_bump)
|
| 206 |
+
projections.append({
|
| 207 |
+
"year": 2025 + yr,
|
| 208 |
+
"salary": round(current),
|
| 209 |
+
"tier": tier,
|
| 210 |
+
"promotion": promotion_bump > 0
|
| 211 |
+
})
|
| 212 |
+
return projections
|
| 213 |
+
|
| 214 |
+
# =========================================================
|
| 215 |
+
# TAB 1: SALARY PREDICTOR
|
| 216 |
+
# =========================================================
|
| 217 |
+
def run_predictor(age, exp, edu, gender, job_title, name, feedback):
|
| 218 |
+
res = predict_local(int(age), int(exp), edu, gender)
|
| 219 |
+
predicted = res["predicted"]
|
| 220 |
+
low, high = res["low"], res["high"]
|
| 221 |
+
tier, exp_group, source = res["tier"], res["exp_group"], res["source"]
|
| 222 |
+
|
| 223 |
+
# Call Automation 1 (n8n salary prediction)
|
| 224 |
+
auto1_result = _n8n(N8N_SALARY_PREDICTION_URL, {
|
| 225 |
+
"age":int(age),"years_of_experience":int(exp),
|
| 226 |
+
"education_level":edu,"gender":gender,"job_title":job_title
|
| 227 |
+
})
|
| 228 |
+
if auto1_result and "predicted_salary" in auto1_result:
|
| 229 |
+
predicted = auto1_result["predicted_salary"]
|
| 230 |
+
low = round(predicted*0.92)
|
| 231 |
+
high = round(predicted*1.08)
|
| 232 |
+
|
| 233 |
+
# Call Automation 2 (HR feedback)
|
| 234 |
+
auto2_result = _n8n(N8N_HR_FEEDBACK_URL, {
|
| 235 |
+
"name": name or "Employee","career_tier":tier,"salary":predicted,
|
| 236 |
+
"years_of_experience":int(exp),"education_level":edu,
|
| 237 |
+
"feedback": feedback or "No feedback provided"
|
| 238 |
+
})
|
| 239 |
+
sentiment_adjustment = 0
|
| 240 |
+
sentiment_label = "Neutral"
|
| 241 |
+
sentiment_score = 0.5
|
| 242 |
+
if auto2_result and "error" not in auto2_result:
|
| 243 |
+
sentiment_adjustment = auto2_result.get("salary_adjustment", 0)
|
| 244 |
+
sentiment_score = auto2_result.get("sentiment_score", 0.5)
|
| 245 |
+
alert = auto2_result.get("alert","")
|
| 246 |
+
if "POSITIVE" in alert.upper(): sentiment_label = "Positive 😊"
|
| 247 |
+
elif "NEGATIVE" in alert.upper(): sentiment_label = "Negative 😟"
|
| 248 |
+
else: sentiment_label = "Neutral 😐"
|
| 249 |
+
if sentiment_adjustment:
|
| 250 |
+
predicted = round(predicted * (1 + sentiment_adjustment/100))
|
| 251 |
+
low = round(predicted*0.92)
|
| 252 |
+
high = round(predicted*1.08)
|
| 253 |
+
|
| 254 |
+
# Salary projection
|
| 255 |
+
projections = project_salary(predicted, int(exp), edu, tier)
|
| 256 |
+
|
| 257 |
+
# Gauge chart
|
| 258 |
+
fig_gauge = go.Figure(go.Indicator(
|
| 259 |
+
mode="gauge+number",value=predicted,
|
| 260 |
+
number={"prefix":"$","valueformat":",.0f","font":{"size":32,"color":PURPLE}},
|
| 261 |
+
gauge={"axis":{"range":[25000,250000],"tickprefix":"$","tickformat":",.0f","tickfont":{"size":9}},"bar":{"color":GOLD,"thickness":0.3},"bgcolor":"white","borderwidth":0,"steps":[{"range":[25000,80000],"color":"#e8f5f0"},{"range":[80000,140000],"color":"#d4ecff"},{"range":[140000,250000],"color":"#ede8ff"}],"threshold":{"line":{"color":"#FF9B9B","width":3},"thickness":0.8,"value":high}},
|
| 262 |
+
title={"text":"Predicted Annual Salary","font":{"size":13,"color":PURPLE}},
|
| 263 |
+
))
|
| 264 |
+
fig_gauge.update_layout(height=280,paper_bgcolor="rgba(255,255,255,0.97)",margin=dict(l=30,r=30,t=60,b=10))
|
| 265 |
+
|
| 266 |
+
# Projection chart
|
| 267 |
+
years = [p["year"] for p in projections]
|
| 268 |
+
salaries = [p["salary"] for p in projections]
|
| 269 |
+
promotions = [p["promotion"] for p in projections]
|
| 270 |
+
proj_colors = ["#F2C637" if p else "#45FFCA" for p in promotions]
|
| 271 |
+
|
| 272 |
+
fig_proj = go.Figure()
|
| 273 |
+
fig_proj.add_trace(go.Scatter(
|
| 274 |
+
x=[2025]+years, y=[predicted]+salaries,
|
| 275 |
+
mode="lines", name="Salary Trajectory",
|
| 276 |
+
line=dict(color="#45FFCA",width=3),
|
| 277 |
+
fill="tozeroy", fillcolor="rgba(69,255,202,0.1)"
|
| 278 |
+
))
|
| 279 |
+
for i,(yr,sal,promo) in enumerate(zip(years,salaries,promotions)):
|
| 280 |
+
if promo:
|
| 281 |
+
fig_proj.add_trace(go.Scatter(
|
| 282 |
+
x=[yr],y=[sal],mode="markers+text",
|
| 283 |
+
marker=dict(color="#F2C637",size=14,symbol="star"),
|
| 284 |
+
text=["Promotion!"],textposition="top center",
|
| 285 |
+
textfont=dict(size=10,color="#F2C637"),
|
| 286 |
+
showlegend=False,
|
| 287 |
+
hovertemplate=f"Year: {yr}<br>Salary: ${sal:,.0f}<br>⭐ Promotion<extra></extra>"
|
| 288 |
+
))
|
| 289 |
+
fig_proj.add_annotation(x=2025,y=predicted,text=f"Now: ${predicted:,.0f}",
|
| 290 |
+
showarrow=True,arrowhead=2,font=dict(size=10,color=PURPLE))
|
| 291 |
+
fig_proj.update_layout(**_layout(height=300,showlegend=False,
|
| 292 |
+
title=dict(text="📈 5-Year Salary Projection"),
|
| 293 |
+
xaxis=dict(tickformat="d"),yaxis=dict(tickprefix="$",tickformat=",.0f")))
|
| 294 |
+
|
| 295 |
+
# Build automation status HTML
|
| 296 |
+
tier_emoji = {"junior":"🌱","mid":"📈","senior":"⭐"}.get(tier,"")
|
| 297 |
+
sent_color = "#45FFCA" if "Positive" in sentiment_label else "#FF9B9B" if "Negative" in sentiment_label else "#D3D1C7"
|
| 298 |
+
adj_text = f"{sentiment_adjustment:+.1f}%" if sentiment_adjustment else "0%"
|
| 299 |
+
|
| 300 |
+
auto1_status = "✅ Connected" if auto1_result and "error" not in auto1_result else ("⚠️ "+auto1_result.get("error","Error")[:30] if auto1_result else "⚙️ Not configured")
|
| 301 |
+
auto2_status = "✅ "+sentiment_label if auto2_result and "error" not in auto2_result else ("⚠️ Error" if auto2_result else "⚙️ Not configured")
|
| 302 |
+
|
| 303 |
+
result_html = f"""
|
| 304 |
+
<div style="font-family:system-ui,sans-serif;">
|
| 305 |
+
<!-- Main prediction card -->
|
| 306 |
+
<div style="background:linear-gradient(135deg,rgba(40,9,109,0.06),rgba(242,198,55,0.09));
|
| 307 |
+
border-radius:16px;padding:20px;border:1.5px solid rgba(40,9,109,0.12);margin-bottom:12px;">
|
| 308 |
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;">
|
| 309 |
+
<div style="text-align:center;">
|
| 310 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:3px;">Career Tier</div>
|
| 311 |
+
<div style="font-size:18px;font-weight:800;color:{PURPLE};">{tier_emoji} {tier.title()}</div>
|
| 312 |
+
</div>
|
| 313 |
+
<div style="text-align:center;">
|
| 314 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:3px;">Experience</div>
|
| 315 |
+
<div style="font-size:14px;font-weight:700;color:{PURPLE};">{exp_group}</div>
|
| 316 |
+
</div>
|
| 317 |
+
<div style="text-align:center;">
|
| 318 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:3px;">Range</div>
|
| 319 |
+
<div style="font-size:13px;font-weight:700;color:{PURPLE};">${low:,.0f} – ${high:,.0f}</div>
|
| 320 |
+
</div>
|
| 321 |
+
<div style="text-align:center;">
|
| 322 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:3px;">Model</div>
|
| 323 |
+
<div style="font-size:10px;font-weight:600;color:#5a4090;">{source}</div>
|
| 324 |
+
</div>
|
| 325 |
+
</div>
|
| 326 |
+
</div>
|
| 327 |
+
|
| 328 |
+
<!-- Sentiment impact -->
|
| 329 |
+
<div style="background:rgba(255,255,255,0.95);border-radius:12px;padding:14px;
|
| 330 |
+
border-left:4px solid {sent_color};margin-bottom:12px;">
|
| 331 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:6px;">💬 Feedback Sentiment Impact</div>
|
| 332 |
+
<div style="display:flex;justify-content:space-between;align-items:center;">
|
| 333 |
+
<span style="font-size:14px;font-weight:700;color:{PURPLE};">{sentiment_label}</span>
|
| 334 |
+
<span style="font-size:13px;font-weight:800;color:{'#2ec4a0' if sentiment_adjustment>0 else '#FF9B9B' if sentiment_adjustment<0 else '#8070b0'};">Salary adj: {adj_text}</span>
|
| 335 |
+
<span style="font-size:12px;color:#8070b0;">Score: {sentiment_score:.2f}</span>
|
| 336 |
+
</div>
|
| 337 |
+
</div>
|
| 338 |
+
|
| 339 |
+
<!-- n8n Automation Status -->
|
| 340 |
+
<div style="background:rgba(40,9,109,0.04);border-radius:12px;padding:14px;border:1px solid rgba(40,9,109,0.1);">
|
| 341 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:8px;">🔗 n8n Automation Status</div>
|
| 342 |
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
| 343 |
+
<div style="background:white;border-radius:8px;padding:10px;border:1px solid rgba(40,9,109,0.08);">
|
| 344 |
+
<div style="font-size:9px;color:#8070b0;margin-bottom:3px;">AUTOMATION 1 — Salary Prediction</div>
|
| 345 |
+
<div style="font-size:12px;font-weight:700;color:{PURPLE};">{auto1_status}</div>
|
| 346 |
+
</div>
|
| 347 |
+
<div style="background:white;border-radius:8px;padding:10px;border:1px solid rgba(40,9,109,0.08);">
|
| 348 |
+
<div style="font-size:9px;color:#8070b0;margin-bottom:3px;">AUTOMATION 2 — HR Feedback Alert</div>
|
| 349 |
+
<div style="font-size:12px;font-weight:700;color:{PURPLE};">{auto2_status}</div>
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
</div>"""
|
| 354 |
+
|
| 355 |
+
# 5-year table
|
| 356 |
+
proj_rows = "".join([
|
| 357 |
+
f'<tr style="background:{"rgba(242,198,55,0.15)" if p["promotion"] else "white"};">'
|
| 358 |
+
f'<td style="padding:8px 12px;font-weight:600;">{p["year"]}</td>'
|
| 359 |
+
f'<td style="padding:8px 12px;">${p["salary"]:,.0f}</td>'
|
| 360 |
+
f'<td style="padding:8px 12px;">{p["tier"].title()}</td>'
|
| 361 |
+
f'<td style="padding:8px 12px;">{"⭐ Promotion!" if p["promotion"] else "—"}</td>'
|
| 362 |
+
f'</tr>'
|
| 363 |
+
for p in projections
|
| 364 |
+
])
|
| 365 |
+
proj_table = f"""
|
| 366 |
+
<div style="font-family:system-ui,sans-serif;background:white;border-radius:12px;overflow:hidden;border:1px solid rgba(40,9,109,0.1);">
|
| 367 |
+
<div style="background:{PURPLE};padding:12px 16px;color:white;font-weight:700;font-size:13px;">📅 5-Year Salary Forecast</div>
|
| 368 |
+
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
| 369 |
+
<thead><tr style="background:rgba(40,9,109,0.05);">
|
| 370 |
+
<th style="padding:8px 12px;text-align:left;color:#8070b0;font-size:10px;text-transform:uppercase;">Year</th>
|
| 371 |
+
<th style="padding:8px 12px;text-align:left;color:#8070b0;font-size:10px;text-transform:uppercase;">Projected Salary</th>
|
| 372 |
+
<th style="padding:8px 12px;text-align:left;color:#8070b0;font-size:10px;text-transform:uppercase;">Tier</th>
|
| 373 |
+
<th style="padding:8px 12px;text-align:left;color:#8070b0;font-size:10px;text-transform:uppercase;">Event</th>
|
| 374 |
+
</tr></thead>
|
| 375 |
+
<tbody>{proj_rows}</tbody>
|
| 376 |
+
</table>
|
| 377 |
+
</div>"""
|
| 378 |
+
|
| 379 |
+
return fig_gauge, fig_proj, result_html, proj_table
|
| 380 |
+
|
| 381 |
+
# =========================================================
|
| 382 |
+
# TAB 2: SALARY ANALYZER
|
| 383 |
+
# =========================================================
|
| 384 |
+
def run_analyzer(salary, age, exp, edu, gender, job_title):
|
| 385 |
+
salary = float(salary)
|
| 386 |
+
tier = get_tier(int(age), int(exp), edu)
|
| 387 |
+
exp_group = get_exp_group(int(exp))
|
| 388 |
+
|
| 389 |
+
# Call Automation 3
|
| 390 |
+
auto3_result = _n8n(N8N_ANOMALY_DETECTION_URL, {
|
| 391 |
+
"salary":salary,"age":int(age),
|
| 392 |
+
"years_of_experience":int(exp),"education_level":edu
|
| 393 |
+
})
|
| 394 |
+
|
| 395 |
+
# Local anomaly calculation as fallback
|
| 396 |
+
base_expected = {"Bachelor's":55000,"Master's":72000,"PhD":92000}.get(edu,55000)
|
| 397 |
+
expected = base_expected + (int(exp)*3200) + ((int(age)-22)*250)
|
| 398 |
+
deviation = ((salary - expected) / expected) * 100
|
| 399 |
+
|
| 400 |
+
if auto3_result and "error" not in auto3_result:
|
| 401 |
+
is_anomaly = auto3_result.get("anomaly", False)
|
| 402 |
+
flag = auto3_result.get("flag", "NORMAL RANGE")
|
| 403 |
+
severity = auto3_result.get("severity", "normal")
|
| 404 |
+
dev = auto3_result.get("deviation_pct", round(deviation,1))
|
| 405 |
+
expected_sal = auto3_result.get("expected_salary", round(expected))
|
| 406 |
+
auto3_status = "✅ Connected"
|
| 407 |
+
else:
|
| 408 |
+
dev = round(deviation,1)
|
| 409 |
+
expected_sal = round(expected)
|
| 410 |
+
is_anomaly = abs(dev) > 20
|
| 411 |
+
if dev > 40: flag,severity = "OVERPAID","high"
|
| 412 |
+
elif dev > 20: flag,severity = "SLIGHTLY OVERPAID","medium"
|
| 413 |
+
elif dev < -40: flag,severity = "UNDERPAID","high"
|
| 414 |
+
elif dev < -20: flag,severity = "SLIGHTLY UNDERPAID","medium"
|
| 415 |
+
else: flag,severity = "NORMAL RANGE","normal"
|
| 416 |
+
auto3_status = "⚙️ Using local calculation" if not auto3_result else "⚠️ Error"
|
| 417 |
+
|
| 418 |
+
# Color theme based on result
|
| 419 |
+
sev_color = {"high":"#FF9B9B","medium":"#F2C637","normal":"#45FFCA"}.get(severity,"#45FFCA")
|
| 420 |
+
sev_icon = {"high":"🚨","medium":"⚠️","normal":"✅"}.get(severity,"✅")
|
| 421 |
+
|
| 422 |
+
# Comparison gauge
|
| 423 |
+
fig_compare = go.Figure()
|
| 424 |
+
fig_compare.add_trace(go.Bar(
|
| 425 |
+
x=["Your Salary","Market Expected"],
|
| 426 |
+
y=[salary, expected_sal],
|
| 427 |
+
marker_color=[sev_color, "#45FFCA"],
|
| 428 |
+
text=[f"${salary:,.0f}", f"${expected_sal:,.0f}"],
|
| 429 |
+
textposition="outside",
|
| 430 |
+
hovertemplate="%{x}: $%{y:,.0f}<extra></extra>"
|
| 431 |
+
))
|
| 432 |
+
fig_compare.update_layout(**_layout(height=320,showlegend=False,
|
| 433 |
+
title=dict(text="Your Salary vs Market Expected"),
|
| 434 |
+
yaxis=dict(tickprefix="$",tickformat=",.0f")))
|
| 435 |
+
|
| 436 |
+
# Deviation gauge
|
| 437 |
+
fig_dev = go.Figure(go.Indicator(
|
| 438 |
+
mode="gauge+number+delta",value=dev,
|
| 439 |
+
number={"suffix":"%","valueformat":".1f","font":{"size":28,"color":PURPLE}},
|
| 440 |
+
delta={"reference":0,"valueformat":".1f","suffix":"%"},
|
| 441 |
+
gauge={"axis":{"range":[-60,60],"ticksuffix":"%"},"bar":{"color":sev_color,"thickness":0.3},"bgcolor":"white","borderwidth":0,"steps":[{"range":[-60,-20],"color":"rgba(255,155,155,0.3)"},{"range":[-20,20],"color":"rgba(69,255,202,0.2)"},{"range":[20,60],"color":"rgba(208,156,250,0.3)"}],"threshold":{"line":{"color":"red","width":3},"thickness":0.8,"value":40 if dev>0 else -40}},
|
| 442 |
+
title={"text":"Salary Deviation from Market","font":{"size":13,"color":PURPLE}},
|
| 443 |
+
))
|
| 444 |
+
fig_dev.update_layout(height=280,paper_bgcolor="rgba(255,255,255,0.97)",margin=dict(l=30,r=30,t=60,b=10))
|
| 445 |
+
|
| 446 |
+
# Percentile estimation
|
| 447 |
+
df = get_base_df()
|
| 448 |
+
if "Salary" in df.columns:
|
| 449 |
+
percentile = round((df["Salary"] < salary).mean() * 100)
|
| 450 |
+
else:
|
| 451 |
+
percentile = 50
|
| 452 |
+
|
| 453 |
+
# Build result HTML
|
| 454 |
+
result_html = f"""
|
| 455 |
+
<div style="font-family:system-ui,sans-serif;">
|
| 456 |
+
<!-- Main anomaly card -->
|
| 457 |
+
<div style="background:linear-gradient(135deg,rgba(255,255,255,0.98),rgba(255,255,255,0.95));
|
| 458 |
+
border-radius:16px;padding:20px;border-left:6px solid {sev_color};margin-bottom:12px;
|
| 459 |
+
box-shadow:0 4px 20px rgba(40,9,109,0.08);">
|
| 460 |
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
|
| 461 |
+
<span style="font-size:32px;">{sev_icon}</span>
|
| 462 |
+
<div>
|
| 463 |
+
<div style="font-size:18px;font-weight:800;color:{PURPLE};">{flag}</div>
|
| 464 |
+
<div style="font-size:12px;color:#8070b0;">Salary anomaly detection result</div>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px;">
|
| 468 |
+
<div style="background:rgba(40,9,109,0.04);border-radius:10px;padding:12px;text-align:center;">
|
| 469 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:4px;">Your Salary</div>
|
| 470 |
+
<div style="font-size:16px;font-weight:800;color:{PURPLE};">${salary:,.0f}</div>
|
| 471 |
+
</div>
|
| 472 |
+
<div style="background:rgba(40,9,109,0.04);border-radius:10px;padding:12px;text-align:center;">
|
| 473 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:4px;">Market Expected</div>
|
| 474 |
+
<div style="font-size:16px;font-weight:800;color:{PURPLE};">${expected_sal:,.0f}</div>
|
| 475 |
+
</div>
|
| 476 |
+
<div style="background:rgba(40,9,109,0.04);border-radius:10px;padding:12px;text-align:center;">
|
| 477 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:4px;">Deviation</div>
|
| 478 |
+
<div style="font-size:16px;font-weight:800;color:{sev_color};">{dev:+.1f}%</div>
|
| 479 |
+
</div>
|
| 480 |
+
<div style="background:rgba(40,9,109,0.04);border-radius:10px;padding:12px;text-align:center;">
|
| 481 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:4px;">Percentile</div>
|
| 482 |
+
<div style="font-size:16px;font-weight:800;color:{PURPLE};">Top {100-percentile}%</div>
|
| 483 |
+
</div>
|
| 484 |
+
</div>
|
| 485 |
+
</div>
|
| 486 |
+
|
| 487 |
+
<!-- Recommendation -->
|
| 488 |
+
<div style="background:rgba(40,9,109,0.04);border-radius:12px;padding:14px;border:1px solid rgba(40,9,109,0.1);margin-bottom:12px;">
|
| 489 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:6px;">💡 Recommendation</div>
|
| 490 |
+
<div style="font-size:13px;color:{PURPLE};font-weight:600;">
|
| 491 |
+
{'Your salary is significantly above market. Ensure your performance justifies this compensation.' if dev > 30 else
|
| 492 |
+
'Your salary is slightly above market — you are in a strong position.' if dev > 10 else
|
| 493 |
+
'Your salary is within market norms. You are fairly compensated.' if abs(dev) <= 10 else
|
| 494 |
+
'Your salary is slightly below market. Consider negotiating a raise.' if dev > -30 else
|
| 495 |
+
'Your salary is significantly below market. You may be underpaid — seek negotiation or new opportunities.'}
|
| 496 |
+
</div>
|
| 497 |
+
</div>
|
| 498 |
+
|
| 499 |
+
<!-- n8n status -->
|
| 500 |
+
<div style="background:rgba(40,9,109,0.04);border-radius:10px;padding:10px 14px;border:1px solid rgba(40,9,109,0.08);">
|
| 501 |
+
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8070b0;margin-bottom:4px;">🔗 AUTOMATION 3 — Anomaly Detection</div>
|
| 502 |
+
<div style="font-size:12px;font-weight:700;color:{PURPLE};">{auto3_status}</div>
|
| 503 |
+
</div>
|
| 504 |
+
</div>"""
|
| 505 |
+
|
| 506 |
+
return fig_dev, fig_compare, result_html
|
| 507 |
+
|
| 508 |
+
# =========================================================
|
| 509 |
+
# TAB 3: INSIGHTS (Dashboard + AI)
|
| 510 |
+
# =========================================================
|
| 511 |
+
def build_edu_chart():
|
| 512 |
+
df=get_base_df()
|
| 513 |
+
if "Education Level" not in df.columns: return go.Figure()
|
| 514 |
+
grp=df.groupby("Education Level")["Salary"].mean().reset_index()
|
| 515 |
+
order=["Bachelor's","Master's","PhD"]
|
| 516 |
+
grp["Education Level"]=pd.Categorical(grp["Education Level"],categories=order,ordered=True)
|
| 517 |
+
grp=grp.sort_values("Education Level")
|
| 518 |
+
fig=go.Figure(go.Bar(x=grp["Education Level"],y=grp["Salary"],marker_color=P[:3],text=[f"${v:,.0f}" for v in grp["Salary"]],textposition="outside",hovertemplate="<b>%{x}</b><br>$%{y:,.0f}<extra></extra>"))
|
| 519 |
+
fig.update_layout(**_layout(height=320,showlegend=False,title=dict(text="Avg Salary by Education"),yaxis=dict(tickprefix="$",tickformat=",.0f")))
|
| 520 |
+
return fig
|
| 521 |
+
|
| 522 |
+
def build_exp_chart():
|
| 523 |
+
df=get_base_df().copy()
|
| 524 |
+
if "Years of Experience" not in df.columns: return go.Figure()
|
| 525 |
+
df["eg"]=df["Years of Experience"].apply(get_exp_group)
|
| 526 |
+
grp=df.groupby("eg")["Salary"].mean().reset_index()
|
| 527 |
+
order=["0-5 years","6-10 years","11-15 years","16-20 years","20+"]
|
| 528 |
+
grp["eg"]=pd.Categorical(grp["eg"],categories=order,ordered=True)
|
| 529 |
+
grp=grp.sort_values("eg")
|
| 530 |
+
fig=go.Figure(go.Bar(x=grp["eg"],y=grp["Salary"],marker_color=P[:5],text=[f"${v:,.0f}" for v in grp["Salary"]],textposition="outside",hovertemplate="<b>%{x}</b><br>$%{y:,.0f}<extra></extra>"))
|
| 531 |
+
fig.update_layout(**_layout(height=320,showlegend=False,title=dict(text="Avg Salary by Experience"),yaxis=dict(tickprefix="$",tickformat=",.0f")))
|
| 532 |
+
return fig
|
| 533 |
+
|
| 534 |
+
def build_gender_chart():
|
| 535 |
+
df=get_base_df()
|
| 536 |
+
if "Gender" not in df.columns: return go.Figure()
|
| 537 |
+
grp=df.groupby("Gender")["Salary"].mean().reset_index()
|
| 538 |
+
fig=go.Figure(go.Bar(x=grp["Gender"],y=grp["Salary"],marker_color=[P[0],P[2]],text=[f"${v:,.0f}" for v in grp["Salary"]],textposition="outside",hovertemplate="<b>%{x}</b><br>$%{y:,.0f}<extra></extra>"))
|
| 539 |
+
fig.update_layout(**_layout(height=320,showlegend=False,title=dict(text="Avg Salary by Gender"),yaxis=dict(tickprefix="$",tickformat=",.0f")))
|
| 540 |
+
return fig
|
| 541 |
+
|
| 542 |
+
def build_tier_chart():
|
| 543 |
+
df=get_base_df()
|
| 544 |
+
if "career_tier" not in df.columns: return go.Figure()
|
| 545 |
+
grp=df.groupby("career_tier")["Salary"].mean().reset_index()
|
| 546 |
+
order=["junior","mid","senior"]
|
| 547 |
+
grp["career_tier"]=pd.Categorical(grp["career_tier"],categories=order,ordered=True)
|
| 548 |
+
grp=grp.sort_values("career_tier")
|
| 549 |
+
fig=go.Figure(go.Bar(x=grp["career_tier"].str.title(),y=grp["Salary"],marker_color=[P[0],P[1],P[2]],text=[f"${v:,.0f}" for v in grp["Salary"]],textposition="outside",hovertemplate="<b>%{x}</b><br>$%{y:,.0f}<extra></extra>"))
|
| 550 |
+
fig.update_layout(**_layout(height=320,showlegend=False,title=dict(text="Avg Salary by Career Tier"),yaxis=dict(tickprefix="$",tickformat=",.0f")))
|
| 551 |
+
return fig
|
| 552 |
+
|
| 553 |
+
def build_prog_chart():
|
| 554 |
+
df=get_prog_df()
|
| 555 |
+
if df.empty or "year" not in df.columns: return go.Figure()
|
| 556 |
+
avg=df.groupby(["year","career_tier"])["salary_that_year"].mean().reset_index()
|
| 557 |
+
fig=go.Figure()
|
| 558 |
+
for i,t in enumerate(["junior","mid","senior"]):
|
| 559 |
+
sub=avg[avg["career_tier"]==t]
|
| 560 |
+
fig.add_trace(go.Scatter(x=sub["year"],y=sub["salary_that_year"],name=t.title(),mode="lines+markers",line=dict(color=P[i],width=2.5),marker=dict(size=7),hovertemplate=f"<b>{t.title()}</b><br>%{{x}}: $%{{y:,.0f}}<extra></extra>"))
|
| 561 |
+
fig.update_layout(**_layout(height=340,title=dict(text="Salary Progression by Career Tier (2020–2024)"),yaxis=dict(tickprefix="$",tickformat=",.0f"),xaxis=dict(tickformat="d")))
|
| 562 |
+
return fig
|
| 563 |
+
|
| 564 |
+
def build_sentiment_chart():
|
| 565 |
+
df=get_feed_df().copy()
|
| 566 |
+
if df.empty: return go.Figure()
|
| 567 |
+
try:
|
| 568 |
+
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
|
| 569 |
+
ana=SentimentIntensityAnalyzer()
|
| 570 |
+
df["score"]=df["feedback_comment"].apply(lambda x: ana.polarity_scores(str(x))["compound"])
|
| 571 |
+
df["sentiment"]=df["score"].apply(lambda s: "positive" if s>=0.05 else ("negative" if s<=-0.05 else "neutral"))
|
| 572 |
+
except:
|
| 573 |
+
df["sentiment"]=df["career_tier"].map({"senior":"positive","mid":"positive","junior":"neutral"}).fillna("neutral")
|
| 574 |
+
counts=df.groupby(["career_tier","sentiment"]).size().unstack(fill_value=0).reset_index()
|
| 575 |
+
for c in ["negative","neutral","positive"]:
|
| 576 |
+
if c not in counts.columns: counts[c]=0
|
| 577 |
+
tot=counts[["negative","neutral","positive"]].sum(axis=1)
|
| 578 |
+
for c in ["negative","neutral","positive"]: counts[c]=(counts[c]/tot*100).round(1)
|
| 579 |
+
order=["junior","mid","senior"]
|
| 580 |
+
counts["career_tier"]=pd.Categorical(counts["career_tier"],categories=order,ordered=True)
|
| 581 |
+
counts=counts.sort_values("career_tier")
|
| 582 |
+
colors={"negative":"#FF9B9B","neutral":"#D3D1C7","positive":"#45FFCA"}
|
| 583 |
+
fig=go.Figure()
|
| 584 |
+
for s in ["negative","neutral","positive"]:
|
| 585 |
+
fig.add_trace(go.Bar(x=counts["career_tier"].str.title(),y=counts[s],name=s.title(),marker_color=colors[s],text=counts[s].apply(lambda v: f"{v:.0f}%"),textposition="inside",hovertemplate=f"<b>{s.title()}</b>: %{{y:.1f}}%<extra></extra>"))
|
| 586 |
+
fig.update_layout(**_layout(height=320,barmode="stack",title=dict(text="Feedback Sentiment by Career Tier (%)"),yaxis=dict(ticksuffix="%",range=[0,100])))
|
| 587 |
+
return fig
|
| 588 |
+
|
| 589 |
+
def build_scatter_chart():
|
| 590 |
+
df=get_base_df()
|
| 591 |
+
if "Years of Experience" not in df.columns: return go.Figure()
|
| 592 |
+
tc={"junior":P[0],"mid":P[1],"senior":P[2]}
|
| 593 |
+
fig=go.Figure()
|
| 594 |
+
for t in ["junior","mid","senior"]:
|
| 595 |
+
sub=df[df["career_tier"]==t]
|
| 596 |
+
fig.add_trace(go.Scatter(x=sub["Years of Experience"],y=sub["Salary"],mode="markers",name=t.title(),marker=dict(color=tc[t],size=5,opacity=0.65),hovertemplate="Exp: %{x}y<br>$%{y:,.0f}<extra></extra>"))
|
| 597 |
+
fig.update_layout(**_layout(height=320,title=dict(text="Experience vs Salary by Career Tier"),xaxis=dict(title="Years of Experience"),yaxis=dict(tickprefix="$",tickformat=",.0f")))
|
| 598 |
+
return fig
|
| 599 |
+
|
| 600 |
+
CHART_MAP={"salary_education":build_edu_chart,"salary_experience":build_exp_chart,"gender_salary":build_gender_chart,"career_tier":build_tier_chart,"salary_progression":build_prog_chart,"sentiment":build_sentiment_chart,"scatter":build_scatter_chart}
|
| 601 |
+
|
| 602 |
+
def render_kpis():
|
| 603 |
+
df=get_base_df()
|
| 604 |
+
n=len(df); avg=df["Salary"].mean() if "Salary" in df.columns else 0
|
| 605 |
+
avg_exp=df["Years of Experience"].mean() if "Years of Experience" in df.columns else 0
|
| 606 |
+
def card(icon,label,value,color):
|
| 607 |
+
return (f'<div style="background:rgba(255,255,255,.9);border-radius:14px;padding:14px 10px;text-align:center;'
|
| 608 |
+
f'border:1.5px solid rgba(255,255,255,.8);box-shadow:0 4px 14px rgba(40,9,109,.07);border-top:3px solid {color};">'
|
| 609 |
+
f'<div style="font-size:20px;margin-bottom:4px;">{icon}</div>'
|
| 610 |
+
f'<div style="color:#8070b0;font-size:8px;text-transform:uppercase;letter-spacing:2px;margin-bottom:4px;font-weight:800;">{label}</div>'
|
| 611 |
+
f'<div style="color:#1a0a3d;font-size:14px;font-weight:800;">{value}</div></div>')
|
| 612 |
+
html=('<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:16px;">'
|
| 613 |
+
+card("👥","Employees",f"{n:,}",GOLD)+card("💰","Avg Salary",f"${avg:,.0f}","#45FFCA")
|
| 614 |
+
+card("📅","Avg Experience",f"{avg_exp:.1f} yrs","#D09CFA")+card("🎯","Career Tiers","3","#FF9B9B")
|
| 615 |
+
+"</div>")
|
| 616 |
+
return html
|
| 617 |
+
|
| 618 |
+
AI_SYSTEM = """You are an AI HR analytics assistant for the F2 Salary Prediction project at ESCP Business School.
|
| 619 |
+
Dataset: Age, Gender, Education Level (Bachelor's/Master's/PhD), Job Title, Years of Experience, Salary, career_tier (junior/mid/senior), synthetic salary progression 2020-2024, VADER sentiment.
|
| 620 |
+
Key findings: PhD earns ~35% more than Bachelor's. Senior tier = highest stable salaries. Junior grows fastest. Gender gap ~4%. RF model ~92-95% accuracy.
|
| 621 |
+
Answer in 2-4 sentences with specific numbers. End with: ```json {"chart": "<n>"}```
|
| 622 |
+
Chart options: salary_education | salary_experience | gender_salary | career_tier | salary_progression | sentiment | scatter | none"""
|
| 623 |
+
|
| 624 |
+
def _keyword_reply(msg):
|
| 625 |
+
m=msg.lower()
|
| 626 |
+
if any(w in m for w in ["education","degree","bachelor","master","phd"]): return ("PhD holders earn ~35% more than Bachelor's graduates. Education is one of the top salary predictors.","salary_education")
|
| 627 |
+
if any(w in m for w in ["gender","male","female","gap"]): return ("There is a ~4% gender pay gap, with male employees earning slightly more on average.","gender_salary")
|
| 628 |
+
if any(w in m for w in ["experience","years","exp"]) and "prog" not in m: return ("Employees with 20+ years earn nearly 3× those with 0-5 years. The steepest growth is in the first 10 years.","salary_experience")
|
| 629 |
+
if any(w in m for w in ["tier","career","junior","mid","senior"]): return ("Senior employees earn the most and have stable salaries. Junior employees show the fastest growth rate.","career_tier")
|
| 630 |
+
if any(w in m for w in ["progress","growth","time","2020","trend"]): return ("Junior employees grow from 50% to 100% of base salary over 5 years. Seniors maintain high stable salaries.","salary_progression")
|
| 631 |
+
if any(w in m for w in ["sentiment","feedback","comment","review"]): return ("Senior employees receive 90%+ positive feedback. There is a moderate positive correlation between sentiment and salary.","sentiment")
|
| 632 |
+
if any(w in m for w in ["scatter","correlation","relationship"]): return ("Experience correlates strongly with salary across all tiers. The scatter shows clear tier separation.","scatter")
|
| 633 |
+
return ("Ask me about: education vs salary, experience trends, gender pay gap, career tier distributions, salary progression, or feedback sentiment.","none")
|
| 634 |
+
|
| 635 |
+
def ai_chat(user_msg, history):
|
| 636 |
+
if not user_msg or not user_msg.strip(): return history or [], "", None
|
| 637 |
+
safe_history=[item for item in (history or []) if isinstance(item,dict) and "role" in item]
|
| 638 |
+
chart_name="none"
|
| 639 |
+
if LLM_ENABLED:
|
| 640 |
+
try:
|
| 641 |
+
msgs=[{"role":"system","content":AI_SYSTEM}]+safe_history[-10:]+[{"role":"user","content":user_msg}]
|
| 642 |
+
r=llm_client.chat_completion(model=MODEL_NAME,messages=msgs,temperature=0.3,max_tokens=400,stream=False)
|
| 643 |
+
raw=r["choices"][0]["message"]["content"] if isinstance(r,dict) else r.choices[0].message.content
|
| 644 |
+
hit=re.search(r'```json\s*(\{.*?\})\s*```',raw,re.DOTALL)
|
| 645 |
+
if hit: chart_name=json.loads(hit.group(1)).get("chart","none")
|
| 646 |
+
reply=re.sub(r'```json.*?```','',raw,flags=re.DOTALL).strip()
|
| 647 |
+
except: reply,chart_name=_keyword_reply(user_msg)
|
| 648 |
+
else: reply,chart_name=_keyword_reply(user_msg)
|
| 649 |
+
chart_fn=CHART_MAP.get(chart_name)
|
| 650 |
+
chart_out=chart_fn() if chart_fn else None
|
| 651 |
+
return safe_history+[{"role":"user","content":user_msg},{"role":"assistant","content":reply}],"",chart_out
|
| 652 |
+
|
| 653 |
+
def refresh_insights():
|
| 654 |
+
return (render_kpis(),build_edu_chart(),build_gender_chart(),
|
| 655 |
+
build_exp_chart(),build_tier_chart(),build_prog_chart(),
|
| 656 |
+
build_sentiment_chart(),build_scatter_chart())
|
| 657 |
+
|
| 658 |
+
# =========================================================
|
| 659 |
+
# PIPELINE RUNNER
|
| 660 |
+
# =========================================================
|
| 661 |
+
def run_notebook_safe(env_key,default):
|
| 662 |
+
nb_name=os.environ.get(env_key,default).strip()
|
| 663 |
+
nb_in=BASE_DIR/nb_name
|
| 664 |
+
if not nb_in.exists():
|
| 665 |
+
return f"❌ {nb_name} not found.\nUpload the notebook to your HuggingFace Space Files tab."
|
| 666 |
+
try:
|
| 667 |
+
import papermill as pm
|
| 668 |
+
out=BASE_DIR/"runs"/f"run_{time.strftime('%Y%m%d-%H%M%S')}_{nb_name}"
|
| 669 |
+
out.parent.mkdir(exist_ok=True)
|
| 670 |
+
pm.execute_notebook(str(nb_in),str(out),cwd=str(BASE_DIR),log_output=True,progress_bar=False,execution_timeout=1800)
|
| 671 |
+
_CACHE.clear()
|
| 672 |
+
return f"✅ {nb_name} completed.\nCSVs: {[p.name for p in BASE_DIR.glob('*.csv')]}"
|
| 673 |
+
except Exception as e:
|
| 674 |
+
return f"❌ FAILED: {e}\n\n{traceback.format_exc()[-1500:]}"
|
| 675 |
+
|
| 676 |
+
def run_nb1(): return run_notebook_safe("NB1","F2_Data_Extraction_and_synthetic_enrichment.ipynb")
|
| 677 |
+
def run_nb2(): return run_notebook_safe("NB2","F2_quantitative_and_qualitative_analysis___prediction.ipynb")
|
| 678 |
+
def run_both():
|
| 679 |
+
return f"{'='*44}\nNOTEBOOK 1\n{'='*44}\n{run_nb1()}\n\n{'='*44}\nNOTEBOOK 2\n{'='*44}\n{run_nb2()}"
|
| 680 |
+
|
| 681 |
+
def load_css():
|
| 682 |
+
p=BASE_DIR/"style.css"
|
| 683 |
+
extra="""
|
| 684 |
+
.gr-button-primary{background:rgb(40,9,109)!important;border:none!important;}
|
| 685 |
+
.tab-nav button{font-weight:700!important;font-size:14px!important;}
|
| 686 |
+
"""
|
| 687 |
+
return (p.read_text(encoding="utf-8") if p.exists() else "")+extra
|
| 688 |
+
|
| 689 |
+
# =========================================================
|
| 690 |
+
# UI
|
| 691 |
+
# =========================================================
|
| 692 |
+
with gr.Blocks(title="F2 Salary Intelligence — ESCP") as demo:
|
| 693 |
+
|
| 694 |
+
gr.Markdown("# F2 Salary Intelligence Platform\n*AI-powered salary prediction, analysis & insights — ESCP Big Data Project*", elem_id="escp_title")
|
| 695 |
+
|
| 696 |
+
# ── TAB 1: SALARY PREDICTOR ─────────────────────────────
|
| 697 |
+
with gr.Tab("🎯 Salary Predictor"):
|
| 698 |
+
gr.Markdown("### Predict your salary and future trajectory\n*Enter your profile — our AI predicts your salary, adjusts for feedback sentiment via n8n, and forecasts your next 5 years.*")
|
| 699 |
+
with gr.Row():
|
| 700 |
+
with gr.Column(scale=1):
|
| 701 |
+
gr.Markdown("#### 👤 Your Profile")
|
| 702 |
+
p_name = gr.Textbox(label="Name (optional)", placeholder="e.g. Jane Doe")
|
| 703 |
+
p_age = gr.Slider(18,70,value=30,step=1,label="Age")
|
| 704 |
+
p_exp = gr.Slider(0,40,value=5,step=1,label="Years of Experience")
|
| 705 |
+
p_edu = gr.Dropdown(["Bachelor's","Master's","PhD"],value="Bachelor's",label="Education Level",interactive=True)
|
| 706 |
+
p_job = gr.Textbox(label="Job Title",value="Data Analyst")
|
| 707 |
+
p_gender = gr.Dropdown(["Male","Female"],value="Male",label="Gender",interactive=True)
|
| 708 |
+
gr.Markdown("#### 💬 Feedback & Sentiment")
|
| 709 |
+
gr.Markdown("*Your feedback influences salary via **Automation 2 (HR Alert)***")
|
| 710 |
+
p_feedback = gr.Textbox(label="Performance Feedback",placeholder='e.g. "Consistently exceeds expectations and shows strong leadership..."',lines=3,interactive=True)
|
| 711 |
+
btn_pred = gr.Button("🚀 Predict My Salary", variant="primary", size="lg")
|
| 712 |
+
|
| 713 |
+
with gr.Column(scale=1):
|
| 714 |
+
gr.Markdown("#### 📊 Prediction Results")
|
| 715 |
+
out_gauge = gr.Plot(label="Salary Estimate")
|
| 716 |
+
out_result = gr.HTML()
|
| 717 |
+
|
| 718 |
+
gr.Markdown("#### 📈 5-Year Salary Trajectory")
|
| 719 |
+
with gr.Row():
|
| 720 |
+
out_proj_chart = gr.Plot(label="Salary Projection")
|
| 721 |
+
out_proj_table = gr.HTML()
|
| 722 |
+
|
| 723 |
+
gr.Markdown("#### 🗂️ Example Profiles")
|
| 724 |
+
gr.Examples(
|
| 725 |
+
examples=[
|
| 726 |
+
["",28,3,"Bachelor's","Junior Data Analyst","Female","Shows promise and responds well to coaching."],
|
| 727 |
+
["",35,10,"Master's","Senior Data Scientist","Male","Consistently exceeds expectations with strong leadership."],
|
| 728 |
+
["",45,20,"PhD","Director of Analytics","Female","Outstanding mentor who drives cross-team innovation."],
|
| 729 |
+
["",22,1,"Bachelor's","Intern","Male","Still ramping up but shows enthusiasm."],
|
| 730 |
+
["",50,25,"Master's","VP of Engineering","Male","Completely fails deadlines and shows poor leadership."],
|
| 731 |
+
],
|
| 732 |
+
inputs=[p_name,p_age,p_exp,p_edu,p_job,p_gender,p_feedback],
|
| 733 |
+
)
|
| 734 |
+
btn_pred.click(run_predictor,inputs=[p_age,p_exp,p_edu,p_gender,p_job,p_name,p_feedback],outputs=[out_gauge,out_proj_chart,out_result,out_proj_table])
|
| 735 |
+
|
| 736 |
+
# ── TAB 2: SALARY ANALYZER ──────────────────────────────
|
| 737 |
+
with gr.Tab("🔍 Salary Analyzer"):
|
| 738 |
+
gr.Markdown("### Is your salary fair?\n*Enter your current salary and profile — **Automation 3** detects anomalies and compares you to market benchmarks.*")
|
| 739 |
+
with gr.Row():
|
| 740 |
+
with gr.Column(scale=1):
|
| 741 |
+
gr.Markdown("#### 💰 Your Salary & Profile")
|
| 742 |
+
a_salary = gr.Number(label="Your Current Annual Salary ($)",value=75000)
|
| 743 |
+
a_age = gr.Slider(18,70,value=30,step=1,label="Age")
|
| 744 |
+
a_exp = gr.Slider(0,40,value=5,step=1,label="Years of Experience")
|
| 745 |
+
a_edu = gr.Dropdown(["Bachelor's","Master's","PhD"],value="Bachelor's",label="Education Level",interactive=True)
|
| 746 |
+
a_gender = gr.Dropdown(["Male","Female"],value="Male",label="Gender",interactive=True)
|
| 747 |
+
a_job = gr.Textbox(label="Job Title",value="Data Analyst")
|
| 748 |
+
btn_ana = gr.Button("🔍 Analyze My Salary", variant="primary", size="lg")
|
| 749 |
+
|
| 750 |
+
with gr.Column(scale=1):
|
| 751 |
+
gr.Markdown("#### 📊 Analysis Results")
|
| 752 |
+
out_dev = gr.Plot(label="Deviation Gauge")
|
| 753 |
+
out_ana_res = gr.HTML()
|
| 754 |
+
|
| 755 |
+
gr.Markdown("#### 📊 Market Comparison")
|
| 756 |
+
out_compare = gr.Plot(label="Salary Comparison")
|
| 757 |
+
|
| 758 |
+
btn_ana.click(run_analyzer,inputs=[a_salary,a_age,a_exp,a_edu,a_gender,a_job],outputs=[out_dev,out_compare,out_ana_res])
|
| 759 |
+
|
| 760 |
+
# ── TAB 3: INSIGHTS ─────────────────────────────────────
|
| 761 |
+
with gr.Tab("📊 Insights"):
|
| 762 |
+
gr.Markdown("### Data Insights & AI Analysis\n*Explore our findings — interactive charts + AI assistant*")
|
| 763 |
+
kpi_html = gr.HTML(value=render_kpis)
|
| 764 |
+
ref_btn = gr.Button("🔄 Refresh Data", variant="secondary", size="sm")
|
| 765 |
+
|
| 766 |
+
with gr.Row():
|
| 767 |
+
with gr.Column(scale=1):
|
| 768 |
+
gr.Markdown("#### 💬 Ask our AI")
|
| 769 |
+
chatbot = gr.Chatbot(label="AI Analytics Assistant",height=380)
|
| 770 |
+
user_input = gr.Textbox(label="Ask about the data",placeholder='e.g. "How does education affect salary?"',lines=1)
|
| 771 |
+
gr.Markdown("**Quick questions:**")
|
| 772 |
+
with gr.Row():
|
| 773 |
+
ex1=gr.Button("📚 Education",size="sm"); ex2=gr.Button("📈 Career tiers",size="sm"); ex3=gr.Button("⚧ Gender gap",size="sm")
|
| 774 |
+
with gr.Row():
|
| 775 |
+
ex4=gr.Button("📅 Progression",size="sm"); ex5=gr.Button("💬 Sentiment",size="sm"); ex6=gr.Button("🔗 Scatter",size="sm")
|
| 776 |
+
|
| 777 |
+
with gr.Column(scale=1):
|
| 778 |
+
ai_chart = gr.Plot(label="Interactive Chart")
|
| 779 |
+
|
| 780 |
+
gr.Markdown("#### 📊 Key Charts")
|
| 781 |
+
with gr.Row():
|
| 782 |
+
c_edu=gr.Plot(label="Education"); c_gender=gr.Plot(label="Gender")
|
| 783 |
+
with gr.Row():
|
| 784 |
+
c_exp=gr.Plot(label="Experience"); c_tier=gr.Plot(label="Career Tier")
|
| 785 |
+
with gr.Row():
|
| 786 |
+
c_prog=gr.Plot(label="Progression"); c_sent=gr.Plot(label="Sentiment")
|
| 787 |
+
c_scatter=gr.Plot(label="Scatter")
|
| 788 |
+
|
| 789 |
+
ex1.click(lambda: "How does education level affect salary?",outputs=[user_input])
|
| 790 |
+
ex2.click(lambda: "Show career tier salary differences",outputs=[user_input])
|
| 791 |
+
ex3.click(lambda: "Is there a gender pay gap?",outputs=[user_input])
|
| 792 |
+
ex4.click(lambda: "Show salary progression over time",outputs=[user_input])
|
| 793 |
+
ex5.click(lambda: "Do senior employees get more positive feedback?",outputs=[user_input])
|
| 794 |
+
ex6.click(lambda: "Show experience vs salary scatter",outputs=[user_input])
|
| 795 |
+
user_input.submit(ai_chat,inputs=[user_input,chatbot],outputs=[chatbot,user_input,ai_chart])
|
| 796 |
+
|
| 797 |
+
def on_refresh():
|
| 798 |
+
kpi,edu,gender,exp,tier,prog,sent,scatter=refresh_insights()
|
| 799 |
+
return kpi,edu,gender,exp,tier,prog,sent,scatter
|
| 800 |
+
ref_btn.click(on_refresh,outputs=[kpi_html,c_edu,c_gender,c_exp,c_tier,c_prog,c_sent,c_scatter])
|
| 801 |
+
|
| 802 |
+
# ── TAB 4: PIPELINE (minimal) ───────────────────────────
|
| 803 |
+
with gr.Tab("⚙️ Pipeline"):
|
| 804 |
+
gr.Markdown("### Data Pipeline\n*Regenerate synthetic data and retrain the model.*")
|
| 805 |
+
with gr.Row():
|
| 806 |
+
btn_nb1=gr.Button("▶ Step 1: Data Creation",variant="secondary")
|
| 807 |
+
btn_nb2=gr.Button("▶ Step 2: Analysis & Training",variant="secondary")
|
| 808 |
+
btn_all=gr.Button("⚡ Run Full Pipeline",variant="primary")
|
| 809 |
+
pipe_log=gr.Textbox(label="Log",lines=15,interactive=False)
|
| 810 |
+
btn_nb1.click(run_nb1,outputs=[pipe_log])
|
| 811 |
+
btn_nb2.click(run_nb2,outputs=[pipe_log])
|
| 812 |
+
btn_all.click(run_both,outputs=[pipe_log])
|
| 813 |
+
|
| 814 |
+
demo.launch(css=load_css(), allowed_paths=[str(BASE_DIR)])
|
style.css
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* --- Target the Gradio app wrapper for backgrounds --- */
|
| 2 |
+
gradio-app,
|
| 3 |
+
.gradio-app,
|
| 4 |
+
.main,
|
| 5 |
+
#app,
|
| 6 |
+
[data-testid="app"] {
|
| 7 |
+
background-color: #eaeefc !important;
|
| 8 |
+
background-image:
|
| 9 |
+
url('https://huggingface.co/spaces/ESCP/F2_Salary_Prediction_Model/resolve/main/background_top.png'),
|
| 10 |
+
url('https://huggingface.co/spaces/ESCP/F2_Salary_Prediction_Model/resolve/main/background_mid.png'),
|
| 11 |
+
url('https://huggingface.co/spaces/ESCP/F2_Salary_Prediction_Model/resolve/main/background_bottom.png') !important;
|
| 12 |
+
background-position:
|
| 13 |
+
top center,
|
| 14 |
+
0 440px,
|
| 15 |
+
bottom center !important;
|
| 16 |
+
background-repeat:
|
| 17 |
+
no-repeat,
|
| 18 |
+
repeat-y,
|
| 19 |
+
no-repeat !important;
|
| 20 |
+
background-size:
|
| 21 |
+
100% auto,
|
| 22 |
+
100% auto,
|
| 23 |
+
100% auto !important;
|
| 24 |
+
min-height: 100vh !important;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* --- Fallback on html/body --- */
|
| 28 |
+
html, body {
|
| 29 |
+
background-color: #eaeefc !important;
|
| 30 |
+
margin: 0 !important;
|
| 31 |
+
padding: 0 !important;
|
| 32 |
+
min-height: 100vh !important;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* --- Main container --- */
|
| 36 |
+
.gradio-container {
|
| 37 |
+
max-width: 100% !important;
|
| 38 |
+
width: 100% !important;
|
| 39 |
+
margin: 0 !important;
|
| 40 |
+
padding: 0 24px 150px 24px !important;
|
| 41 |
+
background: transparent !important;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* --- Title in deep blue --- */
|
| 45 |
+
#escp_title h1 {
|
| 46 |
+
color: #0d1b5e !important;
|
| 47 |
+
font-size: 3.2rem !important;
|
| 48 |
+
font-weight: 900 !important;
|
| 49 |
+
text-align: center !important;
|
| 50 |
+
margin: 0 0 10px 0 !important;
|
| 51 |
+
letter-spacing: -0.5px !important;
|
| 52 |
+
text-shadow: 0 2px 12px rgba(74,110,224,0.15) !important;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/* --- Subtitle in blue --- */
|
| 56 |
+
#escp_title p, #escp_title em {
|
| 57 |
+
color: #3a4888 !important;
|
| 58 |
+
text-align: center !important;
|
| 59 |
+
font-size: 1.05rem !important;
|
| 60 |
+
font-weight: 500 !important;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* --- Tab bar background --- */
|
| 64 |
+
.tabs > .tab-nav,
|
| 65 |
+
.tab-nav,
|
| 66 |
+
div[role="tablist"],
|
| 67 |
+
.svelte-tabs > .tab-nav {
|
| 68 |
+
background: rgba(26,40,104,0.75) !important;
|
| 69 |
+
border-radius: 10px 10px 0 0 !important;
|
| 70 |
+
padding: 4px !important;
|
| 71 |
+
backdrop-filter: blur(8px) !important;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* --- ALL tab buttons: bigger, bolder --- */
|
| 75 |
+
.tabs > .tab-nav button,
|
| 76 |
+
.tab-nav button,
|
| 77 |
+
div[role="tablist"] button,
|
| 78 |
+
button[role="tab"],
|
| 79 |
+
.svelte-tabs button,
|
| 80 |
+
.tab-nav > button,
|
| 81 |
+
.tabs button {
|
| 82 |
+
color: #ffffff !important;
|
| 83 |
+
font-weight: 700 !important;
|
| 84 |
+
font-size: 1rem !important;
|
| 85 |
+
border: none !important;
|
| 86 |
+
background: transparent !important;
|
| 87 |
+
padding: 14px 28px !important;
|
| 88 |
+
border-radius: 8px 8px 0 0 !important;
|
| 89 |
+
opacity: 1 !important;
|
| 90 |
+
letter-spacing: 0.3px !important;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* --- Selected tab: ESCP gold --- */
|
| 94 |
+
.tabs > .tab-nav button.selected,
|
| 95 |
+
.tab-nav button.selected,
|
| 96 |
+
button[role="tab"][aria-selected="true"],
|
| 97 |
+
button[role="tab"].selected,
|
| 98 |
+
div[role="tablist"] button[aria-selected="true"],
|
| 99 |
+
.svelte-tabs button.selected {
|
| 100 |
+
color: rgb(242,198,55) !important;
|
| 101 |
+
background: rgba(255,255,255,0.15) !important;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/* --- Unselected tabs: ensure visibility --- */
|
| 105 |
+
.tabs > .tab-nav button:not(.selected),
|
| 106 |
+
.tab-nav button:not(.selected),
|
| 107 |
+
button[role="tab"][aria-selected="false"],
|
| 108 |
+
button[role="tab"]:not(.selected),
|
| 109 |
+
div[role="tablist"] button:not([aria-selected="true"]) {
|
| 110 |
+
color: #ffffff !important;
|
| 111 |
+
opacity: 1 !important;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/* --- White card panels --- */
|
| 115 |
+
.gradio-container .gr-block,
|
| 116 |
+
.gradio-container .gr-box,
|
| 117 |
+
.gradio-container .gr-panel,
|
| 118 |
+
.gradio-container .gr-group {
|
| 119 |
+
background: #ffffff !important;
|
| 120 |
+
border-radius: 10px !important;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* --- Tab content area --- */
|
| 124 |
+
.tabitem {
|
| 125 |
+
background: rgba(255,255,255,0.96) !important;
|
| 126 |
+
border-radius: 0 0 10px 10px !important;
|
| 127 |
+
padding: 16px !important;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* --- Inputs --- */
|
| 131 |
+
.gradio-container input,
|
| 132 |
+
.gradio-container textarea,
|
| 133 |
+
.gradio-container select {
|
| 134 |
+
background: #ffffff !important;
|
| 135 |
+
border: 1px solid #d1d5db !important;
|
| 136 |
+
border-radius: 8px !important;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* --- Buttons: ESCP purple primary --- */
|
| 140 |
+
.gradio-container button:not([role="tab"]) {
|
| 141 |
+
font-weight: 600 !important;
|
| 142 |
+
padding: 10px 16px !important;
|
| 143 |
+
border-radius: 10px !important;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
button.primary {
|
| 147 |
+
background-color: rgb(40,9,109) !important;
|
| 148 |
+
color: #ffffff !important;
|
| 149 |
+
border: none !important;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
button.primary:hover {
|
| 153 |
+
background-color: rgb(60,20,140) !important;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
button.secondary {
|
| 157 |
+
background-color: #ffffff !important;
|
| 158 |
+
color: rgb(40,9,109) !important;
|
| 159 |
+
border: 2px solid rgb(40,9,109) !important;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
button.secondary:hover {
|
| 163 |
+
background-color: rgb(240,238,250) !important;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* --- Dataframes --- */
|
| 167 |
+
[data-testid="dataframe"] {
|
| 168 |
+
background-color: #ffffff !important;
|
| 169 |
+
border-radius: 10px !important;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
table {
|
| 173 |
+
font-size: 0.85rem !important;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/* --- Chatbot --- */
|
| 177 |
+
.gr-chatbot {
|
| 178 |
+
min-height: 380px !important;
|
| 179 |
+
background-color: #ffffff !important;
|
| 180 |
+
border-radius: 12px !important;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.gr-chatbot .message.user {
|
| 184 |
+
background-color: rgb(232,225,250) !important;
|
| 185 |
+
border-radius: 12px !important;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.gr-chatbot .message.bot {
|
| 189 |
+
background-color: #f3f4f6 !important;
|
| 190 |
+
border-radius: 12px !important;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
/* --- Gallery --- */
|
| 194 |
+
.gallery {
|
| 195 |
+
background: #ffffff !important;
|
| 196 |
+
border-radius: 10px !important;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
/* --- Log textbox --- */
|
| 200 |
+
textarea {
|
| 201 |
+
font-family: monospace !important;
|
| 202 |
+
font-size: 0.8rem !important;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* --- Markdown headings inside tabs --- */
|
| 206 |
+
.tabitem h3 {
|
| 207 |
+
color: rgb(40,9,109) !important;
|
| 208 |
+
font-weight: 700 !important;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.tabitem h4 {
|
| 212 |
+
color: #374151 !important;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/* --- Examples row --- */
|
| 216 |
+
.examples-row button {
|
| 217 |
+
background: rgb(240,238,250) !important;
|
| 218 |
+
color: rgb(40,9,109) !important;
|
| 219 |
+
border: 1px solid rgb(40,9,109) !important;
|
| 220 |
+
border-radius: 8px !important;
|
| 221 |
+
font-size: 0.85rem !important;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.examples-row button:hover {
|
| 225 |
+
background: rgb(232,225,250) !important;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* --- Header / footer: transparent over banner --- */
|
| 229 |
+
header, header *,
|
| 230 |
+
footer, footer * {
|
| 231 |
+
background: transparent !important;
|
| 232 |
+
box-shadow: none !important;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
footer a, footer button,
|
| 236 |
+
header a, header button {
|
| 237 |
+
background: transparent !important;
|
| 238 |
+
border: none !important;
|
| 239 |
+
box-shadow: none !important;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
#footer, #footer *,
|
| 243 |
+
[class*="footer"], [class*="footer"] *,
|
| 244 |
+
[class*="chip"], [class*="pill"], [class*="chip"] *, [class*="pill"] * {
|
| 245 |
+
background: transparent !important;
|
| 246 |
+
border: none !important;
|
| 247 |
+
box-shadow: none !important;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
section footer {
|
| 251 |
+
background: transparent !important;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
section footer button,
|
| 255 |
+
section footer a {
|
| 256 |
+
background: transparent !important;
|
| 257 |
+
background-color: transparent !important;
|
| 258 |
+
border: none !important;
|
| 259 |
+
box-shadow: none !important;
|
| 260 |
+
color: white !important;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
section footer button,
|
| 264 |
+
section footer button * {
|
| 265 |
+
background: transparent !important;
|
| 266 |
+
background-color: transparent !important;
|
| 267 |
+
background-image: none !important;
|
| 268 |
+
box-shadow: none !important;
|
| 269 |
+
filter: none !important;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
section footer button::before,
|
| 273 |
+
section footer button::after {
|
| 274 |
+
background: transparent !important;
|
| 275 |
+
background-color: transparent !important;
|
| 276 |
+
background-image: none !important;
|
| 277 |
+
box-shadow: none !important;
|
| 278 |
+
filter: none !important;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
section footer a,
|
| 282 |
+
section footer a * {
|
| 283 |
+
background: transparent !important;
|
| 284 |
+
background-color: transparent !important;
|
| 285 |
+
box-shadow: none !important;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.gradio-container footer button,
|
| 289 |
+
.gradio-container footer button *,
|
| 290 |
+
.gradio-container .footer button,
|
| 291 |
+
.gradio-container .footer button * {
|
| 292 |
+
background: transparent !important;
|
| 293 |
+
background-color: transparent !important;
|
| 294 |
+
background-image: none !important;
|
| 295 |
+
box-shadow: none !important;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.gradio-container footer button::before,
|
| 299 |
+
.gradio-container footer button::after,
|
| 300 |
+
.gradio-container .footer button::before,
|
| 301 |
+
.gradio-container .footer button::after {
|
| 302 |
+
background: transparent !important;
|
| 303 |
+
background-color: transparent !important;
|
| 304 |
+
background-image: none !important;
|
| 305 |
+
box-shadow: none !important;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* --- Blue slider thumbs & tracks --- */
|
| 309 |
+
input[type="range"] {
|
| 310 |
+
accent-color: #2a4fd4 !important;
|
| 311 |
+
}
|
| 312 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 313 |
+
background: #2a4fd4 !important;
|
| 314 |
+
border: 2px solid #ffffff !important;
|
| 315 |
+
box-shadow: 0 0 0 3px rgba(42,79,212,0.2) !important;
|
| 316 |
+
}
|
| 317 |
+
input[type="range"]::-moz-range-thumb {
|
| 318 |
+
background: #2a4fd4 !important;
|
| 319 |
+
border: 2px solid #ffffff !important;
|
| 320 |
+
}
|
| 321 |
+
input[type="range"]::-webkit-slider-runnable-track {
|
| 322 |
+
background: linear-gradient(to right, #2a4fd4, #6a8ef8) !important;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
/* --- Bigger section headings inside tabs --- */
|
| 326 |
+
.tabitem h3 {
|
| 327 |
+
color: #0d1b5e !important;
|
| 328 |
+
font-weight: 800 !important;
|
| 329 |
+
font-size: 1.25rem !important;
|
| 330 |
+
}
|
| 331 |
+
.tabitem h4 {
|
| 332 |
+
color: #1a2868 !important;
|
| 333 |
+
font-weight: 700 !important;
|
| 334 |
+
font-size: 1.05rem !important;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/* --- Bold markdown content headings --- */
|
| 338 |
+
.tabitem p strong,
|
| 339 |
+
.tabitem label {
|
| 340 |
+
font-weight: 700 !important;
|
| 341 |
+
color: #1a2868 !important;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
/* --- Primary button bigger --- */
|
| 345 |
+
button.primary,
|
| 346 |
+
.gradio-container button.primary {
|
| 347 |
+
background-color: #1a2868 !important;
|
| 348 |
+
color: #ffffff !important;
|
| 349 |
+
font-size: 1.05rem !important;
|
| 350 |
+
padding: 14px 24px !important;
|
| 351 |
+
border-radius: 12px !important;
|
| 352 |
+
font-weight: 700 !important;
|
| 353 |
+
letter-spacing: 0.3px !important;
|
| 354 |
+
box-shadow: 0 4px 14px rgba(26,40,104,0.25) !important;
|
| 355 |
+
}
|
| 356 |
+
button.primary:hover,
|
| 357 |
+
.gradio-container button.primary:hover {
|
| 358 |
+
background-color: #2a3d8e !important;
|
| 359 |
+
box-shadow: 0 6px 20px rgba(26,40,104,0.35) !important;
|
| 360 |
+
}
|