Michtiii's picture
updated error
3c76aa3 verified
"""
╔══════════════════════════════════════════════════════════╗
β•‘ EMAIL JOB TRACKER β€” Hugging Face Spaces v2.1 β•‘
β•‘ SDK: Gradio | LLaMA-3 via Groq | Self-Learn β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
Files needed:
app.py ← this file
requirements.txt ← gradio / imapclient / pyzmail36 / openai / pandas
"""
import gradio as gr
import imapclient
import pyzmail
import pandas as pd
import json
import os
import traceback
from datetime import datetime
from openai import OpenAI
# ──────────────────────────────────────────────────────────
# CONSTANTS
# ──────────────────────────────────────────────────────────
IMAP_SERVER = "imap.gmail.com"
LEARN_FILE = "/tmp/learned_keywords.json"
GMAIL_FOLDERS = [
"INBOX",
"[Gmail]/Important",
"[Gmail]/Starred",
"[Gmail]/Sent Mail",
"[Gmail]/Drafts",
]
CATEGORY_COLORS = {
"Offer": "#10b981",
"Interview": "#3b82f6",
"New Opportunity": "#8b5cf6",
"Rejected": "#f43f5e",
"Urgent": "#f97316",
"Alerts": "#facc15",
"Spam": "#6b7280",
"Unknown": "#94a3b8",
}
CATEGORY_ICONS = {
"Offer": "πŸŽ‰",
"Interview": "πŸ“…",
"New Opportunity": "πŸš€",
"Rejected": "❌",
"Urgent": "πŸ”₯",
"Alerts": "πŸ””",
"Spam": "πŸ—‘οΈ",
"Unknown": "❓",
}
# ──────────────────────────────────────────────────────────
# SELF-LEARNING
# ──────────────────────────────────────────────────────────
def load_learnings():
try:
if os.path.exists(LEARN_FILE):
with open(LEARN_FILE) as f:
return json.load(f)
except Exception:
pass
return {}
def save_learnings(lk):
try:
with open(LEARN_FILE, "w") as f:
json.dump(lk, f, indent=2)
except Exception:
pass
# ──────────────────────────────────────────────────────────
# KEYWORD ENGINE
# ──────────────────────────────────────────────────────────
def get_base_keywords():
return {
"Offer": [
"offer letter","congratulations","selected","employment offer",
"welcome aboard","joining","ctc","salary","compensation",
"offer rollout","final selection","offer acceptance",
],
"Interview": [
"interview","round","technical","hr round","final round",
"assessment","test","assignment","panel","discussion",
"screening","shortlisted","interview invite",
],
"New Opportunity": [
"job opportunity","hiring","opening","vacancy","role opportunity",
"we are hiring","position available","jd attached",
"job description","career opportunity","looking for candidates",
],
"Rejected": [
"regret","unfortunately","not selected","not shortlisted",
"rejected","not a fit","position filled","application unsuccessful",
],
"Urgent": [
"urgent","immediate","asap","priority","today","tomorrow",
"deadline","action required","important","quick response",
],
"Alerts": [
"alert","notification","reminder","update","system alert",
"security alert","account alert","important update",
],
"Spam": [
"sale","discount","offer deal","win","lottery","free",
"click here","subscribe","buy now","limited offer",
],
}
def rule_based_classification(text, lk):
text = text.lower()
kw = get_base_keywords()
for cat in lk:
kw.setdefault(cat, []).extend(lk[cat])
scores = {c: sum(1 for w in words if w in text) for c, words in kw.items()}
best = max(scores, key=scores.get)
return best if scores[best] > 0 else "Unknown"
def learn_from_email(text, category, lk):
filtered = [w for w in text.lower().split() if len(w) > 6]
lk.setdefault(category, [])
for w in filtered[:5]:
if w not in lk[category]:
lk[category].append(w)
save_learnings(lk)
# ──────────────────────────────────────────────────────────
# LLM CLASSIFIER
# ──────────────────────────────────────────────────────────
def classify_email(subject, body, client, lk):
text = (subject + " " + body)[:1500]
prompt = f"""Classify this email into exactly ONE of:
New Opportunity, Interview, Spam, Unknown, Offer, Rejected, Urgent, Alerts
Extract company name, role/position, and interview round if present.
Email:
{text}
Return ONLY valid JSON, no markdown, no backticks:
{{
"category": "",
"company": "",
"role": "",
"round": "",
"confidence": 0.0
}}"""
try:
res = client.chat.completions.create(
model="llama3-70b-8192",
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
output = res.choices[0].message.content.strip()
output = output.replace("```json", "").replace("```", "").strip()
result = json.loads(output)
if result.get("confidence", 0) > 0.8:
learn_from_email(text, result["category"], lk)
return result
except Exception as e:
print(f"[LLM ERROR] {e}")
cat = rule_based_classification(text, lk)
learn_from_email(text, cat, lk)
return {"category": cat, "company": "Unknown",
"role": "Unknown", "round": "N/A", "confidence": 0.6}
# ──────────────────────────────────────────────────────────
# EMAIL FETCH
# ──────────────────────────────────────────────────────────
def fetch_all_emails(email, password, limit):
print(f"[IMAP] Connecting to {IMAP_SERVER} as {email}")
mail = imapclient.IMAPClient(IMAP_SERVER, ssl=True)
mail.login(email, password)
print("[IMAP] Login successful")
collected = []
for folder in GMAIL_FOLDERS:
try:
mail.select_folder(folder, readonly=True)
uids = mail.search(["ALL"])[-limit:]
print(f"[IMAP] {folder}: found {len(uids)} emails")
for uid in uids:
try:
raw = mail.fetch(uid, ["BODY[]"])
msg = pyzmail.PyzMessage.factory(raw[uid][b"BODY[]"])
subj = msg.get_subject() or "(no subject)"
if msg.text_part:
body = msg.text_part.get_payload().decode(
msg.text_part.charset or "utf-8", errors="replace")
elif msg.html_part:
body = msg.html_part.get_payload().decode(
msg.html_part.charset or "utf-8", errors="replace")
else:
body = ""
collected.append({"folder": folder, "subject": subj, "body": body})
except Exception as e:
print(f"[IMAP] UID {uid} error: {e}")
continue
except Exception as e:
print(f"[IMAP] Folder '{folder}' skipped: {e}")
continue
mail.logout()
print(f"[IMAP] Total emails fetched: {len(collected)}")
return collected
# ──────────────────────────────────────────────────────────
# HTML BUILDERS
# ──────────────────────────────────────────────────────────
def make_summary_cards(counts):
cards = ""
for cat, cnt in counts.items():
color = CATEGORY_COLORS.get(cat, "#94a3b8")
icon = CATEGORY_ICONS.get(cat, "β€’")
cards += f"""
<div style="background:linear-gradient(145deg,#0d1117,#161b22);
border:1px solid {color}55; border-left:3px solid {color};
border-radius:10px; padding:14px 20px; min-width:110px;
text-align:center; box-shadow:0 4px 20px {color}22;">
<div style="font-size:1.1rem; margin-bottom:4px;">{icon}</div>
<div style="font-size:2rem; font-weight:800; color:{color};
line-height:1; font-family:'Courier New',monospace;">{cnt}</div>
<div style="font-size:.68rem; color:#8b949e; margin-top:5px;
letter-spacing:.08em; text-transform:uppercase;">{cat}</div>
</div>"""
return f'<div style="display:flex;flex-wrap:wrap;gap:12px;padding:8px 0;">{cards}</div>'
def make_log_html(log_items):
rows = ""
for item in log_items:
color = CATEGORY_COLORS.get(item["category"], "#94a3b8")
icon = CATEGORY_ICONS.get(item["category"], "β€’")
conf_pct = int(item["confidence"] * 100)
conf_color = "#10b981" if conf_pct >= 80 else "#facc15" if conf_pct >= 60 else "#f43f5e"
folder_short = item["folder"].replace("[Gmail]/", "")
rows += f"""
<div style="display:flex;align-items:center;gap:12px;
padding:10px 14px;border-bottom:1px solid #21262d;">
<span style="background:{color}22;color:{color};border:1px solid {color}55;
padding:3px 10px;border-radius:20px;font-size:.7rem;
font-weight:700;white-space:nowrap;min-width:110px;text-align:center;">
{icon} {item["category"]}
</span>
<span style="color:#8b949e;font-size:.72rem;white-space:nowrap;
font-family:'Courier New',monospace;">[{folder_short}]</span>
<span style="color:#c9d1d9;font-size:.82rem;flex:1;overflow:hidden;
text-overflow:ellipsis;white-space:nowrap;">{item["subject"][:72]}</span>
<span style="color:#58a6ff;font-size:.75rem;white-space:nowrap;">{item["company"]}</span>
<span style="color:{conf_color};font-size:.7rem;
font-family:'Courier New',monospace;white-space:nowrap;">{conf_pct}%</span>
</div>"""
header = """
<div style="background:#161b22;padding:10px 14px;display:flex;gap:20px;
font-size:.65rem;color:#8b949e;letter-spacing:.1em;text-transform:uppercase;
border-bottom:1px solid #21262d;">
<span style="min-width:120px;">Category</span>
<span style="min-width:70px;">Folder</span>
<span style="flex:1;">Subject</span>
<span style="min-width:80px;">Company</span>
<span>Conf.</span>
</div>"""
return f"""
<div style="background:#0d1117;border:1px solid #21262d;border-radius:10px;overflow:hidden;
font-family:'Courier New',monospace;">
{header}
<div style="max-height:380px;overflow-y:auto;">{rows}</div>
</div>"""
def make_pipeline_html(tracker_df):
if tracker_df is None or tracker_df.empty:
return ""
sections = ""
for cat in tracker_df["Category"].unique():
color = CATEGORY_COLORS.get(cat, "#94a3b8")
icon = CATEGORY_ICONS.get(cat, "β€’")
subset = tracker_df[tracker_df["Category"] == cat]
items = ""
for _, row in subset.iterrows():
round_text = ""
if str(row.get("Round", "N/A")) not in ["N/A", "", "None", "nan"]:
round_text = f"&nbsp;Β·&nbsp;<span style='color:#58a6ff;'>{row['Round']}</span>"
items += f"""
<div style="background:#161b22;border:1px solid #30363d;border-radius:8px;
padding:10px 14px;margin-bottom:6px;">
<div style="font-size:.85rem;font-weight:700;color:#e6edf3;">{row.get("Company","β€”")}</div>
<div style="font-size:.75rem;color:#8b949e;margin-top:3px;">
{row.get("Role","β€”")}{round_text}
</div>
</div>"""
sections += f"""
<div style="margin-bottom:18px;">
<div style="font-size:.68rem;letter-spacing:.15em;text-transform:uppercase;
color:{color};margin-bottom:8px;font-family:'Courier New',monospace;">
{icon} {cat} ({len(subset)})
</div>
{items}
</div>"""
return f"""
<div style="background:#0d1117;border:1px solid #21262d;border-radius:10px;
padding:18px;max-height:460px;overflow-y:auto;">
{sections}
</div>"""
def make_error_html(msg):
return f"""
<div style="background:#1a0a0a;border:1px solid #f43f5e55;border-left:3px solid #f43f5e;
border-radius:10px;padding:20px;font-family:'Courier New',monospace;
color:#f43f5e;font-size:.85rem;line-height:1.7;">
<strong>❌ Error</strong><br><br>{msg}
</div>"""
def make_info_html(msg, color="#facc15"):
return f"""
<div style="background:#0d1117;border:1px solid {color}44;border-left:3px solid {color};
border-radius:10px;padding:20px;font-family:'Courier New',monospace;
color:{color};font-size:.85rem;">
{msg}
</div>"""
# ──────────────────────────────────────────────────────────
# MAIN PIPELINE
# ──────────────────────────────────────────────────────────
def run_pipeline(email, password, groq_key, limit, progress=gr.Progress(track_tqdm=True)):
# ── Validation ──
if not email.strip() or not password.strip() or not groq_key.strip():
yield make_error_html("Please fill in <strong>all three</strong> credential fields."), \
"", "", None, None
return
# ── Init ──
client = OpenAI(api_key=groq_key.strip(), base_url="https://api.groq.com/openai/v1")
lk = load_learnings()
yield make_info_html("πŸ”Œ Connecting to Gmail IMAP…"), "", "", None, None
# ── Fetch ──
try:
emails = fetch_all_emails(email.strip(), password.strip(), int(limit))
except Exception as e:
tb = traceback.format_exc()
print(f"[FETCH ERROR]\n{tb}")
# Show a friendly + technical message
err_detail = str(e)
tip = ""
if "AUTHENTICATIONFAILED" in err_detail or "Invalid credentials" in err_detail:
tip = "<br><br><strong>Fix:</strong> Wrong email or App Password. Make sure you generated an App Password (not your real password) at <a href='https://myaccount.google.com/apppasswords' style='color:#58a6ff;'>myaccount.google.com/apppasswords</a>."
elif "IMAP access is disabled" in err_detail:
tip = "<br><br><strong>Fix:</strong> Enable IMAP in Gmail β†’ Settings β†’ See all settings β†’ Forwarding and POP/IMAP β†’ Enable IMAP."
elif "timed out" in err_detail.lower() or "connect" in err_detail.lower():
tip = "<br><br><strong>Fix:</strong> Network timeout β€” HF Spaces may be blocking outbound IMAP (port 993). Try running locally instead."
yield make_error_html(f"{err_detail}{tip}"), "", "", None, None
return
if not emails:
yield make_info_html("⚠️ No emails found in any folder.", "#f97316"), "", "", None, None
return
# ── Classify ──
records, log_items = [], []
total = len(emails)
for i, mail in enumerate(emails):
progress((i + 1) / total)
result = classify_email(mail["subject"], mail["body"], client, lk)
rec = {
"Timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"Folder": mail["folder"],
"Company": result.get("company", "Unknown"),
"Role": result.get("role", "Unknown"),
"Category": result.get("category", "Unknown"),
"Round": result.get("round", "N/A"),
"Confidence": round(result.get("confidence", 0.5), 2),
"Subject": mail["subject"],
}
records.append(rec)
log_items.append({
"folder": mail["folder"],
"subject": mail["subject"],
"category": rec["Category"],
"company": rec["Company"],
"confidence": rec["Confidence"],
})
# stream update every 5 emails
if (i + 1) % 5 == 0 or (i + 1) == total:
df_p = pd.DataFrame(records)
yield (
make_summary_cards(df_p["Category"].value_counts()),
make_log_html(log_items),
"",
None, None,
)
# ── Save & Final ──
df = pd.DataFrame(records)
tracker = df.groupby(["Company", "Role"]).last().reset_index()
df.to_csv("/tmp/email_log.csv", index=False)
tracker.to_csv("/tmp/job_tracker.csv", index=False)
print(f"[DONE] {len(records)} emails classified, {len(tracker)} unique jobs tracked")
yield (
make_summary_cards(tracker["Category"].value_counts()),
make_log_html(log_items),
make_pipeline_html(tracker),
"/tmp/email_log.csv",
"/tmp/job_tracker.csv",
)
# ──────────────────────────────────────────────────────────
# GRADIO CSS
# ──────────────────────────────────────────────────────────
CSS = """
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600;800&family=DM+Sans:wght@300;400;500;700&display=swap');
*, *::before, *::after { box-sizing: border-box; }
body, .gradio-container {
background: #010409 !important;
color: #e6edf3 !important;
font-family: 'DM Sans', sans-serif !important;
}
/* Tabs */
.tab-nav { background: #0d1117 !important; border-bottom: 1px solid #21262d !important; }
.tab-nav button {
color: #8b949e !important;
font-family: 'DM Sans', sans-serif !important;
font-size: .85rem !important;
}
.tab-nav button.selected {
color: #e6edf3 !important;
border-bottom: 2px solid #58a6ff !important;
background: transparent !important;
}
/* Labels */
label > span {
color: #8b949e !important;
font-size: .68rem !important;
letter-spacing: .12em !important;
text-transform: uppercase !important;
font-family: 'JetBrains Mono', monospace !important;
}
/* Inputs */
input[type=text], input[type=password], textarea {
background: #0d1117 !important;
border: 1px solid #30363d !important;
color: #e6edf3 !important;
border-radius: 6px !important;
font-family: 'DM Sans', sans-serif !important;
font-size: .88rem !important;
}
input[type=text]:focus, input[type=password]:focus {
border-color: #58a6ff !important;
box-shadow: 0 0 0 3px #58a6ff18 !important;
}
/* Slider */
input[type=range] { accent-color: #58a6ff !important; }
/* Buttons */
.gr-button {
font-family: 'DM Sans', sans-serif !important;
border-radius: 6px !important;
font-weight: 600 !important;
font-size: .88rem !important;
transition: all .18s !important;
}
.gr-button-primary {
background: #238636 !important;
border: 1px solid #2ea043 !important;
color: #fff !important;
}
.gr-button-primary:hover {
background: #2ea043 !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 16px #2ea04344 !important;
}
.gr-button-secondary {
background: #21262d !important;
border: 1px solid #30363d !important;
color: #c9d1d9 !important;
}
.gr-button-secondary:hover { background: #30363d !important; }
/* Panels */
.gr-panel, .gr-box, .gr-form {
background: #0d1117 !important;
border: 1px solid #21262d !important;
border-radius: 10px !important;
}
/* Dataframe */
.gr-dataframe table {
background: #0d1117 !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: .78rem !important;
}
.gr-dataframe th {
background: #161b22 !important;
color: #8b949e !important;
letter-spacing: .06em !important;
text-transform: uppercase !important;
}
.gr-dataframe td { color: #c9d1d9 !important; border-color: #21262d !important; }
.gr-dataframe tr:hover td { background: #161b22 !important; }
/* Scrollbar */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: #0d1117; }
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #484f58; }
"""
# ──────────────────────────────────────────────────────────
# STATIC HTML BLOCKS
# ──────────────────────────────────────────────────────────
HEADER_HTML = """
<div style="padding:28px 0 22px;font-family:'DM Sans',sans-serif;
border-bottom:1px solid #21262d;margin-bottom:20px;">
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap;">
<div style="background:linear-gradient(135deg,#238636,#3fb950);
border-radius:12px;padding:12px 15px;font-size:1.5rem;line-height:1;">πŸ“¬</div>
<div>
<div style="font-size:.62rem;letter-spacing:.25em;text-transform:uppercase;
color:#58a6ff;font-family:'JetBrains Mono',monospace;margin-bottom:4px;">
LLaMA-3 Β· Groq Β· Self-Learning Β· Gradio
</div>
<h1 style="font-size:1.8rem;font-weight:800;color:#e6edf3;
margin:0;letter-spacing:-.03em;line-height:1;">
Email Job Tracker
</h1>
<p style="color:#8b949e;margin:5px 0 0;font-size:.86rem;font-weight:300;">
Fetch β†’ classify β†’ track your entire job pipeline automatically
</p>
</div>
<div style="margin-left:auto;text-align:right;display:flex;flex-direction:column;gap:6px;">
<span style="background:#238636;color:#fff;font-size:.62rem;padding:3px 10px;
border-radius:4px;font-weight:700;font-family:'JetBrains Mono',monospace;">
v2.1
</span>
<span style="background:#1f6feb22;color:#58a6ff;font-size:.62rem;padding:3px 10px;
border-radius:4px;font-family:'JetBrains Mono',monospace;
border:1px solid #1f6feb55;">
HF SPACES
</span>
</div>
</div>
</div>
"""
CRED_HELP = """
<div style="background:#161b22;border:1px solid #30363d;border-left:3px solid #58a6ff;
border-radius:8px;padding:14px 16px;font-size:.78rem;color:#8b949e;
font-family:'DM Sans',sans-serif;line-height:1.8;margin-top:8px;">
<strong style="color:#c9d1d9;">πŸ” Setup Help</strong><br>
β€’ Use a <strong style="color:#e6edf3;">Gmail App Password</strong> β€” not your real password<br>
β€’ Requires 2FA ON β†’
<a href="https://myaccount.google.com/apppasswords" target="_blank" style="color:#58a6ff;">
Generate App Password
</a><br>
β€’ Enable IMAP: Gmail β†’ Settings β†’ Forwarding &amp; POP/IMAP β†’ Enable IMAP<br>
β€’ Free Groq key β†’
<a href="https://console.groq.com" target="_blank" style="color:#58a6ff;">
console.groq.com
</a><br>
β€’ Credentials are <strong style="color:#e6edf3;">never stored</strong>
</div>
"""
GUIDE_HTML = """
<div style="max-width:700px;font-family:'DM Sans',sans-serif;line-height:1.8;padding:8px 0;">
<h2 style="color:#e6edf3;font-size:1.05rem;margin-bottom:6px;">πŸš€ Quick Start</h2>
<ol style="color:#8b949e;font-size:.88rem;padding-left:20px;">
<li>Enable <strong style="color:#c9d1d9;">2-Step Verification</strong> on your Google account</li>
<li>Generate a <a href="https://myaccount.google.com/apppasswords" target="_blank" style="color:#58a6ff;">Gmail App Password</a> (16 chars β€” enter without spaces)</li>
<li>Enable IMAP: Gmail β†’ βš™οΈ Settings β†’ See all settings β†’ Forwarding and POP/IMAP β†’ Enable IMAP β†’ Save</li>
<li>Get a free API key from <a href="https://console.groq.com" target="_blank" style="color:#58a6ff;">Groq Console</a></li>
<li>Enter credentials in <strong style="color:#c9d1d9;">Run Agent</strong> tab and click Run</li>
</ol>
<h2 style="color:#e6edf3;font-size:1.05rem;margin-top:22px;margin-bottom:10px;">🏷️ Categories</h2>
<table style="font-size:.82rem;border-collapse:collapse;width:100%;">
<thead>
<tr style="background:#161b22;">
<th style="padding:8px 12px;color:#8b949e;text-align:left;border:1px solid #21262d;">Category</th>
<th style="padding:8px 12px;color:#8b949e;text-align:left;border:1px solid #21262d;">Trigger Keywords</th>
</tr>
</thead>
<tbody>
<tr><td style="padding:8px 12px;color:#10b981;border:1px solid #21262d;">πŸŽ‰ Offer</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">Offer letter, CTC, Welcome aboard, Salary</td></tr>
<tr><td style="padding:8px 12px;color:#3b82f6;border:1px solid #21262d;">πŸ“… Interview</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">Invite, Round, Assessment, Screening, Shortlisted</td></tr>
<tr><td style="padding:8px 12px;color:#8b5cf6;border:1px solid #21262d;">πŸš€ New Opportunity</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">Hiring, Opening, JD Attached, Vacancy</td></tr>
<tr><td style="padding:8px 12px;color:#f43f5e;border:1px solid #21262d;">❌ Rejected</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">Regret, Unfortunately, Not selected, Not a fit</td></tr>
<tr><td style="padding:8px 12px;color:#f97316;border:1px solid #21262d;">πŸ”₯ Urgent</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">ASAP, Deadline, Action Required, Priority</td></tr>
<tr><td style="padding:8px 12px;color:#facc15;border:1px solid #21262d;">πŸ”” Alerts</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">Security Alert, Notification, System Update</td></tr>
<tr><td style="padding:8px 12px;color:#6b7280;border:1px solid #21262d;">πŸ—‘οΈ Spam</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">Sale, Lottery, Buy Now, Free, Subscribe</td></tr>
</tbody>
</table>
<h2 style="color:#e6edf3;font-size:1.05rem;margin-top:22px;margin-bottom:4px;">🧠 Self-Learning</h2>
<p style="color:#8b949e;font-size:.88rem;">
Every classification with &gt;80% confidence teaches the system new keywords for that category,
saved to <code style="color:#58a6ff;background:#161b22;padding:1px 6px;border-radius:3px;">/tmp/learned_keywords.json</code>.
Accuracy improves the more emails you process.
</p>
<h2 style="color:#e6edf3;font-size:1.05rem;margin-top:22px;margin-bottom:4px;">πŸ› οΈ Troubleshooting</h2>
<table style="font-size:.82rem;border-collapse:collapse;width:100%;">
<thead>
<tr style="background:#161b22;">
<th style="padding:8px 12px;color:#8b949e;text-align:left;border:1px solid #21262d;">Error</th>
<th style="padding:8px 12px;color:#8b949e;text-align:left;border:1px solid #21262d;">Fix</th>
</tr>
</thead>
<tbody>
<tr><td style="padding:8px 12px;color:#f43f5e;border:1px solid #21262d;">AUTHENTICATIONFAILED</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">Wrong App Password or email β€” regenerate App Password</td></tr>
<tr><td style="padding:8px 12px;color:#f43f5e;border:1px solid #21262d;">IMAP access disabled</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">Enable IMAP in Gmail settings (step 3 above)</td></tr>
<tr><td style="padding:8px 12px;color:#f43f5e;border:1px solid #21262d;">Connection timeout</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">HF Spaces may block port 993 β€” run locally with python app.py</td></tr>
<tr><td style="padding:8px 12px;color:#f43f5e;border:1px solid #21262d;">Groq error</td><td style="padding:8px 12px;color:#8b949e;border:1px solid #21262d;">Check API key at console.groq.com β€” falls back to rule-based</td></tr>
</tbody>
</table>
</div>
"""
EMPTY_LOG = """
<div style="background:#0d1117;border:1px solid #21262d;border-radius:10px;
padding:40px;text-align:center;color:#484f58;
font-family:'JetBrains Mono',monospace;font-size:.8rem;letter-spacing:.05em;">
β–‘β–‘ no data yet β€” run the agent β–‘β–‘
</div>"""
def sec(label, color="#58a6ff"):
return (f'<div style="font-size:.62rem;letter-spacing:.18em;text-transform:uppercase;'
f'color:{color};font-family:\'JetBrains Mono\',monospace;'
f'margin-bottom:8px;margin-top:2px;">{label}</div>')
# ──────────────────────────────────────────────────────────
# GRADIO LAYOUT
# ──────────────────────────────────────────────────────────
with gr.Blocks(css=CSS, title="πŸ“¬ Email Job Tracker") as demo:
gr.HTML(HEADER_HTML)
with gr.Tabs():
# ══ TAB 1: RUN AGENT ══════════════════════════
with gr.TabItem("⚑ Run Agent"):
with gr.Row(equal_height=False):
with gr.Column(scale=1, min_width=300):
gr.HTML(sec("Credentials"))
email_in = gr.Textbox(label="Gmail Address", placeholder="you@gmail.com")
pass_in = gr.Textbox(label="Gmail App Password", type="password",
placeholder="xxxx xxxx xxxx xxxx")
groq_in = gr.Textbox(label="Groq API Key", type="password",
placeholder="gsk_...")
gr.HTML(sec("Settings", "#8b949e"))
limit_in = gr.Slider(label="Emails per folder",
minimum=5, maximum=50, value=20, step=5)
with gr.Row():
run_btn = gr.Button("⚑ Run Agent", variant="primary", size="lg")
clear_btn = gr.Button("βœ• Clear", variant="secondary", size="lg")
gr.HTML(CRED_HELP)
with gr.Column(scale=2):
gr.HTML(sec("Summary"))
summary_out = gr.HTML(value=EMPTY_LOG)
gr.HTML(sec("Live Log"))
log_out = gr.HTML(value=EMPTY_LOG)
gr.HTML(sec("Pipeline View"))
pipeline_out = gr.HTML(value=EMPTY_LOG)
gr.HTML(sec("Downloads", "#8b949e"))
with gr.Row():
file_log = gr.File(label="πŸ“„ email_log.csv")
file_tracker = gr.File(label="πŸ“Š job_tracker.csv")
# ══ TAB 2: TRACKER TABLE ══════════════════════
with gr.TabItem("πŸ“Š Tracker Table"):
gr.HTML(sec("Latest status per company / role"))
table_out = gr.Dataframe(
headers=["Company","Role","Category","Round","Confidence","Subject"],
interactive=False,
wrap=False,
)
# ══ TAB 3: GUIDE ══════════════════════════════
with gr.TabItem("πŸ“– Guide"):
gr.HTML(GUIDE_HTML)
# ── EVENT HANDLERS ──────────────────────────────
def do_run(email, password, groq_key, limit, progress=gr.Progress(track_tqdm=True)):
last = None
for result in run_pipeline(email, password, groq_key, limit, progress):
last = result
yield result[0], result[1], result[2], result[3], result[4], gr.update()
if last is None:
return
# populate tracker table
try:
df = pd.read_csv("/tmp/job_tracker.csv")
cols = [c for c in ["Company","Role","Category","Round","Confidence","Subject"]
if c in df.columns]
yield last[0], last[1], last[2], last[3], last[4], df[cols]
except Exception:
yield last[0], last[1], last[2], last[3], last[4], gr.update()
def do_clear():
return EMPTY_LOG, EMPTY_LOG, EMPTY_LOG, None, None, None
run_btn.click(
fn=do_run,
inputs=[email_in, pass_in, groq_in, limit_in],
outputs=[summary_out, log_out, pipeline_out, file_log, file_tracker, table_out],
)
clear_btn.click(
fn=do_clear,
outputs=[summary_out, log_out, pipeline_out, file_log, file_tracker, table_out],
)
if __name__ == "__main__":
demo.launch(ssr_mode=False)