| """ |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| β 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 |
|
|
| |
| |
| |
| 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": "β", |
| } |
|
|
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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} |
|
|
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| 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" Β· <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>""" |
|
|
| |
| |
| |
| def run_pipeline(email, password, groq_key, limit, progress=gr.Progress(track_tqdm=True)): |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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}") |
| |
| 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 |
|
|
| |
| 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"], |
| }) |
|
|
| |
| 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, |
| ) |
|
|
| |
| 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", |
| ) |
|
|
| |
| |
| |
| 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; } |
| """ |
|
|
| |
| |
| |
| 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 & 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 >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>') |
|
|
| |
| |
| |
| with gr.Blocks(css=CSS, title="π¬ Email Job Tracker") as demo: |
| gr.HTML(HEADER_HTML) |
|
|
| with gr.Tabs(): |
|
|
| |
| 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") |
|
|
| |
| 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, |
| ) |
|
|
| |
| with gr.TabItem("π Guide"): |
| gr.HTML(GUIDE_HTML) |
|
|
| |
| 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 |
|
|
| |
| 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) |
|
|