Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -135,7 +135,7 @@ def analyze_job(text: str) -> Tuple[str, int, str, go.Figure]:
|
|
| 135 |
cdf = pd.DataFrame(detected, columns=["Signal", "Weight"])
|
| 136 |
cdf["Type"] = cdf["Weight"].apply(lambda w: "Red flag" if w > 0 else "Positive")
|
| 137 |
fig = px.bar(cdf, x="Weight", y="Signal", color="Type", orientation="h",
|
| 138 |
-
color_discrete_map={"Red flag": "#
|
| 139 |
title="Signal breakdown")
|
| 140 |
fig.update_layout(**_styled_layout(height=420, showlegend=True))
|
| 141 |
else:
|
|
@@ -150,22 +150,22 @@ def analyze_job(text: str) -> Tuple[str, int, str, go.Figure]:
|
|
| 150 |
|
| 151 |
def _styled_layout(**kwargs) -> dict:
|
| 152 |
defaults = dict(
|
| 153 |
-
template="
|
| 154 |
-
paper_bgcolor="#
|
| 155 |
-
plot_bgcolor="#
|
| 156 |
-
font=dict(family="Geist, system-ui, -apple-system, sans-serif", color="#
|
| 157 |
margin=dict(l=60, r=20, t=70, b=70),
|
| 158 |
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1,
|
| 159 |
-
bgcolor="rgba(
|
| 160 |
-
bordercolor="#
|
| 161 |
-
font=dict(color="#
|
| 162 |
-
title=dict(font=dict(size=14, color="#
|
| 163 |
-
xaxis=dict(gridcolor="#
|
| 164 |
-
tickfont=dict(color="#
|
| 165 |
-
title=dict(font=dict(color="#
|
| 166 |
-
yaxis=dict(gridcolor="#
|
| 167 |
-
tickfont=dict(color="#
|
| 168 |
-
title=dict(font=dict(color="#
|
| 169 |
)
|
| 170 |
defaults.update(kwargs)
|
| 171 |
return defaults
|
|
@@ -174,12 +174,12 @@ def _styled_layout(**kwargs) -> dict:
|
|
| 174 |
def _empty_chart(title: str) -> go.Figure:
|
| 175 |
fig = go.Figure()
|
| 176 |
fig.update_layout(
|
| 177 |
-
title=title, height=420, template="
|
| 178 |
-
paper_bgcolor="#
|
| 179 |
-
plot_bgcolor="#
|
| 180 |
-
font=dict(color="#
|
| 181 |
annotations=[dict(text="(no data available)", x=0.5, y=0.5, xref="paper", yref="paper",
|
| 182 |
-
showarrow=False, font=dict(size=13, color="#
|
| 183 |
)
|
| 184 |
return fig
|
| 185 |
|
|
@@ -198,7 +198,7 @@ def build_flag_frequency_chart() -> go.Figure:
|
|
| 198 |
fig = go.Figure(go.Bar(
|
| 199 |
y=counts.index[::-1], x=counts.values[::-1], orientation="h",
|
| 200 |
marker=dict(color=counts.values[::-1],
|
| 201 |
-
colorscale=[[0, "#
|
| 202 |
hovertemplate="<b>%{y}</b><br>Detected in %{x} jobs<extra></extra>",
|
| 203 |
))
|
| 204 |
fig.update_layout(**_styled_layout(
|
|
@@ -212,7 +212,7 @@ def build_risk_distribution_chart() -> go.Figure:
|
|
| 212 |
if DF.empty or "Risk Level" not in DF.columns:
|
| 213 |
return _empty_chart("Dataset not loaded")
|
| 214 |
counts = DF["Risk Level"].value_counts()
|
| 215 |
-
colors = {"Low": "#
|
| 216 |
fig = go.Figure(go.Pie(
|
| 217 |
labels=counts.index, values=counts.values,
|
| 218 |
marker=dict(colors=[colors.get(l, "#888") for l in counts.index]),
|
|
@@ -228,8 +228,8 @@ def build_score_distribution_chart() -> go.Figure:
|
|
| 228 |
return _empty_chart("Dataset not loaded")
|
| 229 |
scores = DF["Score"].dropna()
|
| 230 |
fig = go.Figure(go.Histogram(
|
| 231 |
-
x=scores, nbinsx=15, marker_color="#
|
| 232 |
-
marker_line_color="#
|
| 233 |
hovertemplate="Score range: %{x}<br>Jobs: %{y}<extra></extra>",
|
| 234 |
))
|
| 235 |
fig.update_layout(**_styled_layout(
|
|
@@ -246,13 +246,13 @@ def build_score_distribution_chart() -> go.Figure:
|
|
| 246 |
|
| 247 |
def render_kpi_cards() -> str:
|
| 248 |
if DF.empty:
|
| 249 |
-
return ('<div style="background:#
|
| 250 |
-
'border-radius:12px;border:1px solid #
|
| 251 |
'<div style="font-family:\'Geist Mono\',monospace;font-size:11px;'
|
| 252 |
-
'color:#
|
| 253 |
-
'<div style="color:#
|
| 254 |
-
'Upload <code style="background:#
|
| 255 |
-
'font-family:\'Geist Mono\',monospace;font-size:0.85em;border:1px solid #
|
| 256 |
'job_description_data.xlsx</code> to populate metrics.'
|
| 257 |
'</div></div>')
|
| 258 |
|
|
@@ -267,35 +267,36 @@ def render_kpi_cards() -> str:
|
|
| 267 |
all_flags.extend(label for label, _ in extract_flag_labels(str(cell)))
|
| 268 |
top_flag = pd.Series(all_flags).value_counts().index[0] if all_flags else "β"
|
| 269 |
|
| 270 |
-
def card(label, value, delta_text, accent_color="#
|
| 271 |
return f"""
|
| 272 |
-
<div style="background:#
|
| 273 |
padding:20px 22px;position:relative;overflow:hidden;
|
| 274 |
-
|
|
|
|
| 275 |
<div style="font-family:'Geist Mono','SF Mono',monospace;
|
| 276 |
-
color:
|
| 277 |
text-transform:uppercase;letter-spacing:0.08em;margin-bottom:14px;">
|
| 278 |
{label}
|
| 279 |
</div>
|
| 280 |
-
<div style="color:#
|
| 281 |
letter-spacing:-0.03em;margin-bottom:10px;
|
| 282 |
font-family:'Geist',-apple-system,system-ui,sans-serif;">
|
| 283 |
{value}
|
| 284 |
</div>
|
| 285 |
<div style="display:flex;align-items:center;gap:6px;
|
| 286 |
-
font-family:'Geist Mono',monospace;font-size:11px;color:#
|
| 287 |
<span style="display:inline-block;width:6px;height:6px;border-radius:50%;
|
| 288 |
-
background:{accent_color};box-shadow:0 0 8px {accent_color};"></span>
|
| 289 |
<span>{delta_text}</span>
|
| 290 |
</div>
|
| 291 |
</div>"""
|
| 292 |
|
| 293 |
cards = [
|
| 294 |
-
card("Total.Jobs", f"{total_jobs}", "real labeled postings", "#
|
| 295 |
-
card("Avg.Score", f"{avg_score:.1f}", "weighted across dataset", "#
|
| 296 |
-
card("High.Risk %", f"{high_pct:.0f}%", f"{risk_counts.get('High', 0)} postings flagged", "#
|
| 297 |
card("Top.Signal", top_flag.split(' ')[0].title() if top_flag != "β" else "β",
|
| 298 |
-
top_flag if top_flag != "β" else "no data", "#
|
| 299 |
]
|
| 300 |
return ('<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));'
|
| 301 |
'gap:12px;margin-bottom:32px;">' + "".join(cards) + "</div>")
|
|
@@ -388,8 +389,9 @@ def ai_chat(user_msg: str, history: list):
|
|
| 388 |
reply += "\n\n" + reply_fb
|
| 389 |
elif LLM_ENABLED:
|
| 390 |
msgs = [{"role": "system", "content": DASHBOARD_SYSTEM}]
|
| 391 |
-
for
|
| 392 |
-
msgs.append(
|
|
|
|
| 393 |
msgs.append({"role": "user", "content": user_msg})
|
| 394 |
try:
|
| 395 |
r = llm_client.chat_completion(model=MODEL_NAME, messages=msgs,
|
|
@@ -412,10 +414,7 @@ def ai_chat(user_msg: str, history: list):
|
|
| 412 |
}
|
| 413 |
chart_out = chart_builders[directive["show"]]() if directive.get("show") in chart_builders else None
|
| 414 |
|
| 415 |
-
new_history = (history or []) + [
|
| 416 |
-
{"role": "user", "content": user_msg},
|
| 417 |
-
{"role": "assistant", "content": reply},
|
| 418 |
-
]
|
| 419 |
return new_history, "", chart_out
|
| 420 |
|
| 421 |
|
|
|
|
| 135 |
cdf = pd.DataFrame(detected, columns=["Signal", "Weight"])
|
| 136 |
cdf["Type"] = cdf["Weight"].apply(lambda w: "Red flag" if w > 0 else "Positive")
|
| 137 |
fig = px.bar(cdf, x="Weight", y="Signal", color="Type", orientation="h",
|
| 138 |
+
color_discrete_map={"Red flag": "#c53030", "Positive": "#2f855a"},
|
| 139 |
title="Signal breakdown")
|
| 140 |
fig.update_layout(**_styled_layout(height=420, showlegend=True))
|
| 141 |
else:
|
|
|
|
| 150 |
|
| 151 |
def _styled_layout(**kwargs) -> dict:
|
| 152 |
defaults = dict(
|
| 153 |
+
template="plotly_white",
|
| 154 |
+
paper_bgcolor="#fdfaf3",
|
| 155 |
+
plot_bgcolor="#fdfaf3",
|
| 156 |
+
font=dict(family="Geist, system-ui, -apple-system, sans-serif", color="#1a2238", size=12),
|
| 157 |
margin=dict(l=60, r=20, t=70, b=70),
|
| 158 |
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1,
|
| 159 |
+
bgcolor="rgba(253,250,243,0.9)",
|
| 160 |
+
bordercolor="#d9cfb9", borderwidth=1,
|
| 161 |
+
font=dict(color="#4a5475", size=11)),
|
| 162 |
+
title=dict(font=dict(size=14, color="#1a2238", family="Geist, system-ui, sans-serif")),
|
| 163 |
+
xaxis=dict(gridcolor="#e6dcc7", zerolinecolor="#d9cfb9",
|
| 164 |
+
tickfont=dict(color="#4a5475", size=11),
|
| 165 |
+
title=dict(font=dict(color="#4a5475", size=12))),
|
| 166 |
+
yaxis=dict(gridcolor="#e6dcc7", zerolinecolor="#d9cfb9",
|
| 167 |
+
tickfont=dict(color="#4a5475", size=11),
|
| 168 |
+
title=dict(font=dict(color="#4a5475", size=12))),
|
| 169 |
)
|
| 170 |
defaults.update(kwargs)
|
| 171 |
return defaults
|
|
|
|
| 174 |
def _empty_chart(title: str) -> go.Figure:
|
| 175 |
fig = go.Figure()
|
| 176 |
fig.update_layout(
|
| 177 |
+
title=title, height=420, template="plotly_white",
|
| 178 |
+
paper_bgcolor="#fdfaf3",
|
| 179 |
+
plot_bgcolor="#fdfaf3",
|
| 180 |
+
font=dict(color="#1a2238", family="Geist, system-ui, sans-serif"),
|
| 181 |
annotations=[dict(text="(no data available)", x=0.5, y=0.5, xref="paper", yref="paper",
|
| 182 |
+
showarrow=False, font=dict(size=13, color="#8a9099"))],
|
| 183 |
)
|
| 184 |
return fig
|
| 185 |
|
|
|
|
| 198 |
fig = go.Figure(go.Bar(
|
| 199 |
y=counts.index[::-1], x=counts.values[::-1], orientation="h",
|
| 200 |
marker=dict(color=counts.values[::-1],
|
| 201 |
+
colorscale=[[0, "#f4b8b1"], [1, "#e85a4f"]]),
|
| 202 |
hovertemplate="<b>%{y}</b><br>Detected in %{x} jobs<extra></extra>",
|
| 203 |
))
|
| 204 |
fig.update_layout(**_styled_layout(
|
|
|
|
| 212 |
if DF.empty or "Risk Level" not in DF.columns:
|
| 213 |
return _empty_chart("Dataset not loaded")
|
| 214 |
counts = DF["Risk Level"].value_counts()
|
| 215 |
+
colors = {"Low": "#2a9d8f", "Medium": "#e9a23b", "High": "#c53030"}
|
| 216 |
fig = go.Figure(go.Pie(
|
| 217 |
labels=counts.index, values=counts.values,
|
| 218 |
marker=dict(colors=[colors.get(l, "#888") for l in counts.index]),
|
|
|
|
| 228 |
return _empty_chart("Dataset not loaded")
|
| 229 |
scores = DF["Score"].dropna()
|
| 230 |
fig = go.Figure(go.Histogram(
|
| 231 |
+
x=scores, nbinsx=15, marker_color="#e85a4f",
|
| 232 |
+
marker_line_color="#c53030", marker_line_width=1,
|
| 233 |
hovertemplate="Score range: %{x}<br>Jobs: %{y}<extra></extra>",
|
| 234 |
))
|
| 235 |
fig.update_layout(**_styled_layout(
|
|
|
|
| 246 |
|
| 247 |
def render_kpi_cards() -> str:
|
| 248 |
if DF.empty:
|
| 249 |
+
return ('<div style="background:#fdfaf3;padding:32px;text-align:center;'
|
| 250 |
+
'border-radius:12px;border:1px solid #d9cfb9;">'
|
| 251 |
'<div style="font-family:\'Geist Mono\',monospace;font-size:11px;'
|
| 252 |
+
'color:#e85a4f;letter-spacing:0.08em;text-transform:uppercase;margin-bottom:12px;font-weight:600;">No Data</div>'
|
| 253 |
+
'<div style="color:#4a5475;font-size:14px;">'
|
| 254 |
+
'Upload <code style="background:#f1ebe0;color:#7d4e8a;padding:2px 6px;border-radius:4px;'
|
| 255 |
+
'font-family:\'Geist Mono\',monospace;font-size:0.85em;border:1px solid #e6dcc7;">'
|
| 256 |
'job_description_data.xlsx</code> to populate metrics.'
|
| 257 |
'</div></div>')
|
| 258 |
|
|
|
|
| 267 |
all_flags.extend(label for label, _ in extract_flag_labels(str(cell)))
|
| 268 |
top_flag = pd.Series(all_flags).value_counts().index[0] if all_flags else "β"
|
| 269 |
|
| 270 |
+
def card(label, value, delta_text, accent_color="#e85a4f"):
|
| 271 |
return f"""
|
| 272 |
+
<div style="background:#fdfaf3;border:1px solid #d9cfb9;border-radius:12px;
|
| 273 |
padding:20px 22px;position:relative;overflow:hidden;
|
| 274 |
+
box-shadow:0 1px 0 rgba(255,255,255,0.7) inset, 0 2px 8px rgba(26, 34, 56, 0.04);
|
| 275 |
+
transition:border-color 0.15s, transform 0.15s;">
|
| 276 |
<div style="font-family:'Geist Mono','SF Mono',monospace;
|
| 277 |
+
color:{accent_color};font-size:11px;font-weight:600;
|
| 278 |
text-transform:uppercase;letter-spacing:0.08em;margin-bottom:14px;">
|
| 279 |
{label}
|
| 280 |
</div>
|
| 281 |
+
<div style="color:#1a2238;font-size:34px;font-weight:700;line-height:1;
|
| 282 |
letter-spacing:-0.03em;margin-bottom:10px;
|
| 283 |
font-family:'Geist',-apple-system,system-ui,sans-serif;">
|
| 284 |
{value}
|
| 285 |
</div>
|
| 286 |
<div style="display:flex;align-items:center;gap:6px;
|
| 287 |
+
font-family:'Geist Mono',monospace;font-size:11px;color:#4a5475;">
|
| 288 |
<span style="display:inline-block;width:6px;height:6px;border-radius:50%;
|
| 289 |
+
background:{accent_color};box-shadow:0 0 8px {accent_color}80;"></span>
|
| 290 |
<span>{delta_text}</span>
|
| 291 |
</div>
|
| 292 |
</div>"""
|
| 293 |
|
| 294 |
cards = [
|
| 295 |
+
card("Total.Jobs", f"{total_jobs}", "real labeled postings", "#e85a4f"),
|
| 296 |
+
card("Avg.Score", f"{avg_score:.1f}", "weighted across dataset", "#2a9d8f"),
|
| 297 |
+
card("High.Risk %", f"{high_pct:.0f}%", f"{risk_counts.get('High', 0)} postings flagged", "#c53030"),
|
| 298 |
card("Top.Signal", top_flag.split(' ')[0].title() if top_flag != "β" else "β",
|
| 299 |
+
top_flag if top_flag != "β" else "no data", "#7d4e8a"),
|
| 300 |
]
|
| 301 |
return ('<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));'
|
| 302 |
'gap:12px;margin-bottom:32px;">' + "".join(cards) + "</div>")
|
|
|
|
| 389 |
reply += "\n\n" + reply_fb
|
| 390 |
elif LLM_ENABLED:
|
| 391 |
msgs = [{"role": "system", "content": DASHBOARD_SYSTEM}]
|
| 392 |
+
for user_turn, bot_turn in (history or [])[-3:]:
|
| 393 |
+
msgs.append({"role": "user", "content": user_turn})
|
| 394 |
+
msgs.append({"role": "assistant", "content": bot_turn})
|
| 395 |
msgs.append({"role": "user", "content": user_msg})
|
| 396 |
try:
|
| 397 |
r = llm_client.chat_completion(model=MODEL_NAME, messages=msgs,
|
|
|
|
| 414 |
}
|
| 415 |
chart_out = chart_builders[directive["show"]]() if directive.get("show") in chart_builders else None
|
| 416 |
|
| 417 |
+
new_history = (history or []) + [(user_msg, reply)]
|
|
|
|
|
|
|
|
|
|
| 418 |
return new_history, "", chart_out
|
| 419 |
|
| 420 |
|