Phileassss commited on
Commit
b89a7f7
·
verified ·
1 Parent(s): beead84

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +814 -0
  2. style.css +360 -0
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
+ }