Michtiii commited on
Commit
329d73a
Β·
verified Β·
1 Parent(s): a675087

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +717 -0
  2. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,717 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════╗
3
+ β•‘ EMAIL JOB TRACKER β€” Hugging Face Spaces β•‘
4
+ β•‘ SDK: Gradio | Model: LLaMA-3 via Groq β•‘
5
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
6
+
7
+ Deploy:
8
+ 1. Create a new HF Space β†’ SDK: Gradio
9
+ 2. Upload app.py + requirements.txt
10
+ 3. Space auto-builds and launches
11
+ """
12
+
13
+ import gradio as gr
14
+ import imapclient
15
+ import pyzmail
16
+ import pandas as pd
17
+ import json
18
+ import os
19
+ from datetime import datetime
20
+ from openai import OpenAI
21
+
22
+ # ──────────────────────────────────────────────
23
+ # CONSTANTS
24
+ # ──────────────────────────────────────────────
25
+ IMAP_SERVER = "imap.gmail.com"
26
+ LEARN_FILE = "/tmp/learned_keywords.json"
27
+
28
+ GMAIL_FOLDERS = [
29
+ "INBOX",
30
+ "[Gmail]/Sent Mail",
31
+ "[Gmail]/Drafts",
32
+ "[Gmail]/Important",
33
+ "[Gmail]/Starred",
34
+ ]
35
+
36
+ CATEGORY_COLORS = {
37
+ "Offer": "#10b981",
38
+ "Interview": "#3b82f6",
39
+ "New Opportunity": "#8b5cf6",
40
+ "Rejected": "#f43f5e",
41
+ "Urgent": "#f97316",
42
+ "Alerts": "#facc15",
43
+ "Spam": "#6b7280",
44
+ "Unknown": "#94a3b8",
45
+ }
46
+
47
+ CATEGORY_ICONS = {
48
+ "Offer": "πŸŽ‰",
49
+ "Interview": "πŸ“…",
50
+ "New Opportunity": "πŸš€",
51
+ "Rejected": "❌",
52
+ "Urgent": "πŸ”₯",
53
+ "Alerts": "πŸ””",
54
+ "Spam": "πŸ—‘οΈ",
55
+ "Unknown": "❓",
56
+ }
57
+
58
+ # ──────────────────────────────────────────────
59
+ # SELF-LEARNING KEYWORDS
60
+ # ──────────────────────────────────────────────
61
+ def load_learnings():
62
+ try:
63
+ if os.path.exists(LEARN_FILE):
64
+ with open(LEARN_FILE) as f:
65
+ return json.load(f)
66
+ except Exception:
67
+ pass
68
+ return {}
69
+
70
+ def save_learnings(lk):
71
+ try:
72
+ with open(LEARN_FILE, "w") as f:
73
+ json.dump(lk, f, indent=2)
74
+ except Exception:
75
+ pass
76
+
77
+ def get_base_keywords():
78
+ return {
79
+ "Offer": [
80
+ "offer letter", "congratulations", "selected", "employment offer",
81
+ "welcome aboard", "joining", "ctc", "salary", "compensation",
82
+ "offer rollout", "final selection", "offer acceptance",
83
+ ],
84
+ "Interview": [
85
+ "interview", "round", "technical", "hr round", "final round",
86
+ "assessment", "test", "assignment", "panel", "discussion",
87
+ "screening", "shortlisted", "interview invite",
88
+ ],
89
+ "New Opportunity": [
90
+ "job opportunity", "hiring", "opening", "vacancy", "role opportunity",
91
+ "we are hiring", "position available", "jd attached",
92
+ "job description", "career opportunity", "looking for candidates",
93
+ ],
94
+ "Rejected": [
95
+ "regret", "unfortunately", "not selected", "not shortlisted",
96
+ "rejected", "not a fit", "position filled", "application unsuccessful",
97
+ ],
98
+ "Urgent": [
99
+ "urgent", "immediate", "asap", "priority", "today", "tomorrow",
100
+ "deadline", "action required", "important", "quick response",
101
+ ],
102
+ "Alerts": [
103
+ "alert", "notification", "reminder", "update", "system alert",
104
+ "security alert", "account alert", "important update",
105
+ ],
106
+ "Spam": [
107
+ "sale", "discount", "offer deal", "win", "lottery", "free",
108
+ "click here", "subscribe", "buy now", "limited offer",
109
+ ],
110
+ }
111
+
112
+ def rule_based_classification(text, lk):
113
+ text = text.lower()
114
+ kw = get_base_keywords()
115
+ for cat in lk:
116
+ kw.setdefault(cat, []).extend(lk[cat])
117
+ scores = {c: sum(1 for w in words if w in text) for c, words in kw.items()}
118
+ best = max(scores, key=scores.get)
119
+ return best if scores[best] > 0 else "Unknown"
120
+
121
+ def learn_from_email(text, category, lk):
122
+ filtered = [w for w in text.lower().split() if len(w) > 6]
123
+ lk.setdefault(category, [])
124
+ for w in filtered[:5]:
125
+ if w not in lk[category]:
126
+ lk[category].append(w)
127
+ save_learnings(lk)
128
+
129
+ # ──────────────────────────────────────────────
130
+ # LLM CLASSIFIER (Groq / LLaMA-3)
131
+ # ──────────────────────────────────────────────
132
+ def classify_email(subject, body, client, lk):
133
+ text = (subject + " " + body)[:1500]
134
+ prompt = f"""Classify this email into exactly ONE of:
135
+ New Opportunity, Interview, Spam, Unknown, Offer, Rejected, Urgent, Alerts
136
+
137
+ Extract company name, role/position, and interview round if present.
138
+
139
+ Email:
140
+ {text}
141
+
142
+ Return ONLY a valid JSON object β€” no markdown, no backticks:
143
+ {{
144
+ "category": "",
145
+ "company": "",
146
+ "role": "",
147
+ "round": "",
148
+ "confidence": 0.0
149
+ }}"""
150
+ try:
151
+ res = client.chat.completions.create(
152
+ model="llama3-70b-8192",
153
+ messages=[{"role": "user", "content": prompt}],
154
+ temperature=0,
155
+ )
156
+ output = res.choices[0].message.content.strip()
157
+ output = output.replace("```json", "").replace("```", "").strip()
158
+ result = json.loads(output)
159
+ if result.get("confidence", 0) > 0.8:
160
+ learn_from_email(text, result["category"], lk)
161
+ return result
162
+ except Exception:
163
+ cat = rule_based_classification(text, lk)
164
+ learn_from_email(text, cat, lk)
165
+ return {"category": cat, "company": "Unknown",
166
+ "role": "Unknown", "round": "N/A", "confidence": 0.6}
167
+
168
+ # ──────────────────────────────────────────────
169
+ # EMAIL FETCH
170
+ # ──────────────────────────────────────────────
171
+ def fetch_all_emails(email, password, limit):
172
+ mail = imapclient.IMAPClient(IMAP_SERVER, ssl=True)
173
+ mail.login(email, password)
174
+ collected = []
175
+ for folder in GMAIL_FOLDERS:
176
+ try:
177
+ mail.select_folder(folder, readonly=True)
178
+ uids = mail.search(["ALL"])[-limit:]
179
+ for uid in uids:
180
+ raw = mail.fetch(uid, ["BODY[]"])
181
+ msg = pyzmail.PyzMessage.factory(raw[uid][b"BODY[]"])
182
+ subj = msg.get_subject() or "(no subject)"
183
+ if msg.text_part:
184
+ body = msg.text_part.get_payload().decode(
185
+ msg.text_part.charset or "utf-8", errors="replace")
186
+ elif msg.html_part:
187
+ body = msg.html_part.get_payload().decode(
188
+ msg.html_part.charset or "utf-8", errors="replace")
189
+ else:
190
+ body = ""
191
+ collected.append({"folder": folder, "subject": subj, "body": body})
192
+ except Exception:
193
+ continue
194
+ mail.logout()
195
+ return collected
196
+
197
+ # ──────────────────────────────────────────────
198
+ # HTML HELPERS
199
+ # ──────────────────────────────────────────────
200
+ def make_summary_cards(counts):
201
+ cards = ""
202
+ for cat, cnt in counts.items():
203
+ color = CATEGORY_COLORS.get(cat, "#94a3b8")
204
+ icon = CATEGORY_ICONS.get(cat, "β€’")
205
+ cards += f"""
206
+ <div style="
207
+ background: linear-gradient(145deg, #0d1117, #161b22);
208
+ border: 1px solid {color}55;
209
+ border-left: 3px solid {color};
210
+ border-radius: 10px;
211
+ padding: 14px 20px;
212
+ min-width: 110px;
213
+ text-align: center;
214
+ box-shadow: 0 4px 20px {color}22;
215
+ ">
216
+ <div style="font-size:1.1rem; margin-bottom:4px;">{icon}</div>
217
+ <div style="font-size:2rem; font-weight:800; color:{color}; line-height:1; font-family:'Courier New',monospace;">{cnt}</div>
218
+ <div style="font-size:0.68rem; color:#8b949e; margin-top:5px; letter-spacing:.08em; text-transform:uppercase;">{cat}</div>
219
+ </div>"""
220
+ return f"""
221
+ <div style="display:flex; flex-wrap:wrap; gap:12px; padding:8px 0;">
222
+ {cards}
223
+ </div>"""
224
+
225
+ def make_log_html(log_items):
226
+ rows = ""
227
+ for item in log_items:
228
+ color = CATEGORY_COLORS.get(item["category"], "#94a3b8")
229
+ icon = CATEGORY_ICONS.get(item["category"], "β€’")
230
+ conf_pct = int(item["confidence"] * 100)
231
+ conf_color = "#10b981" if conf_pct >= 80 else "#facc15" if conf_pct >= 60 else "#f43f5e"
232
+ rows += f"""
233
+ <div style="
234
+ display:flex; align-items:center; gap:12px;
235
+ padding:10px 14px; border-bottom:1px solid #21262d;
236
+ transition: background .15s;
237
+ " onmouseover="this.style.background='#161b22'" onmouseout="this.style.background='transparent'">
238
+ <span style="
239
+ background:{color}22; color:{color};
240
+ border:1px solid {color}55;
241
+ padding:3px 10px; border-radius:20px;
242
+ font-size:.7rem; font-weight:700;
243
+ white-space:nowrap; min-width:90px; text-align:center;
244
+ ">{icon} {item["category"]}</span>
245
+ <span style="color:#8b949e; font-size:.72rem; white-space:nowrap; font-family:'Courier New',monospace;">[{item["folder"].replace("[Gmail]/","")}]</span>
246
+ <span style="color:#c9d1d9; font-size:.82rem; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{item["subject"][:75]}</span>
247
+ <span style="color:#58a6ff; font-size:.75rem; white-space:nowrap;">{item["company"]}</span>
248
+ <span style="color:{conf_color}; font-size:.7rem; font-family:'Courier New',monospace; white-space:nowrap;">{conf_pct}%</span>
249
+ </div>"""
250
+ return f"""
251
+ <div style="
252
+ background:#0d1117;
253
+ border:1px solid #21262d;
254
+ border-radius:10px;
255
+ overflow:hidden;
256
+ font-family: 'Courier New', monospace;
257
+ ">
258
+ <div style="
259
+ background:#161b22;
260
+ padding:10px 14px;
261
+ display:flex; gap:20px;
262
+ font-size:.68rem; color:#8b949e;
263
+ letter-spacing:.1em; text-transform:uppercase;
264
+ border-bottom:1px solid #21262d;
265
+ ">
266
+ <span style="min-width:110px;">Category</span>
267
+ <span style="min-width:70px;">Folder</span>
268
+ <span style="flex:1;">Subject</span>
269
+ <span style="min-width:80px;">Company</span>
270
+ <span>Conf.</span>
271
+ </div>
272
+ <div style="max-height:380px; overflow-y:auto;">
273
+ {rows}
274
+ </div>
275
+ </div>"""
276
+
277
+ def make_pipeline_html(tracker_df):
278
+ """Group by category, show company cards."""
279
+ if tracker_df is None or tracker_df.empty:
280
+ return ""
281
+ sections = ""
282
+ for cat in tracker_df["Category"].unique():
283
+ color = CATEGORY_COLORS.get(cat, "#94a3b8")
284
+ icon = CATEGORY_ICONS.get(cat, "β€’")
285
+ subset = tracker_df[tracker_df["Category"] == cat]
286
+ items = ""
287
+ for _, row in subset.iterrows():
288
+ items += f"""
289
+ <div style="
290
+ background:#161b22;
291
+ border:1px solid #30363d;
292
+ border-radius:8px;
293
+ padding:10px 14px;
294
+ margin-bottom:6px;
295
+ ">
296
+ <div style="font-size:.85rem; font-weight:700; color:#e6edf3;">{row.get("Company","β€”")}</div>
297
+ <div style="font-size:.75rem; color:#8b949e; margin-top:3px;">
298
+ {row.get("Role","β€”")}
299
+ {"&nbsp;Β·&nbsp;<span style='color:#58a6ff;'>"+row.get("Round","")+"</span>" if row.get("Round","N/A") not in ["N/A","","None"] else ""}
300
+ </div>
301
+ </div>"""
302
+ sections += f"""
303
+ <div style="margin-bottom:18px;">
304
+ <div style="
305
+ font-size:.7rem; letter-spacing:.15em; text-transform:uppercase;
306
+ color:{color}; margin-bottom:8px;
307
+ font-family:'Courier New',monospace;
308
+ ">{icon} {cat} ({len(subset)})</div>
309
+ {items}
310
+ </div>"""
311
+ return f"""
312
+ <div style="
313
+ background:#0d1117;
314
+ border:1px solid #21262d;
315
+ border-radius:10px;
316
+ padding:18px;
317
+ max-height:460px;
318
+ overflow-y:auto;
319
+ ">
320
+ {sections}
321
+ </div>"""
322
+
323
+ # ──────────────────────────────────────────────
324
+ # MAIN PIPELINE
325
+ # ──────────────────────────────────────────────
326
+ def run_pipeline(email, password, groq_key, limit, progress=gr.Progress(track_tqdm=True)):
327
+ if not email.strip() or not password.strip() or not groq_key.strip():
328
+ yield (
329
+ gr.update(value="<div style='color:#f43f5e;padding:16px;font-family:monospace;'>⚠️ Please fill in all three credential fields.</div>"),
330
+ gr.update(value=""), gr.update(value=""), None, None,
331
+ )
332
+ return
333
+
334
+ client = OpenAI(api_key=groq_key.strip(), base_url="https://api.groq.com/openai/v1")
335
+ lk = load_learnings()
336
+
337
+ yield (
338
+ gr.update(value="<div style='color:#facc15;padding:16px;font-family:monospace;'>πŸ”Œ Connecting to Gmail…</div>"),
339
+ gr.update(value=""), gr.update(value=""), None, None,
340
+ )
341
+
342
+ try:
343
+ emails = fetch_all_emails(email.strip(), password.strip(), int(limit))
344
+ except Exception as e:
345
+ yield (
346
+ gr.update(value=f"<div style='color:#f43f5e;padding:16px;font-family:monospace;'>❌ Gmail failed: {e}<br><br>Check email address and App Password.</div>"),
347
+ gr.update(value=""), gr.update(value=""), None, None,
348
+ )
349
+ return
350
+
351
+ if not emails:
352
+ yield (
353
+ gr.update(value="<div style='color:#94a3b8;padding:16px;font-family:monospace;'>No emails found in selected folders.</div>"),
354
+ gr.update(value=""), gr.update(value=""), None, None,
355
+ )
356
+ return
357
+
358
+ records, log_items = [], []
359
+ total = len(emails)
360
+
361
+ for i, mail in enumerate(emails):
362
+ progress((i + 1) / total)
363
+ result = classify_email(mail["subject"], mail["body"], client, lk)
364
+ rec = {
365
+ "Timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
366
+ "Folder": mail["folder"],
367
+ "Company": result.get("company", "Unknown"),
368
+ "Role": result.get("role", "Unknown"),
369
+ "Category": result.get("category", "Unknown"),
370
+ "Round": result.get("round", "N/A"),
371
+ "Confidence": round(result.get("confidence", 0.5), 2),
372
+ "Subject": mail["subject"],
373
+ }
374
+ records.append(rec)
375
+ log_items.append({
376
+ "folder": mail["folder"],
377
+ "subject": mail["subject"],
378
+ "category": rec["Category"],
379
+ "company": rec["Company"],
380
+ "confidence": rec["Confidence"],
381
+ })
382
+
383
+ # Live update every 5 emails
384
+ if (i + 1) % 5 == 0 or (i + 1) == total:
385
+ df_partial = pd.DataFrame(records)
386
+ partial_log = make_log_html(log_items)
387
+ partial_sum = make_summary_cards(df_partial["Category"].value_counts())
388
+ yield (
389
+ gr.update(value=partial_sum),
390
+ gr.update(value=partial_log),
391
+ gr.update(value=""),
392
+ None, None,
393
+ )
394
+
395
+ df = pd.DataFrame(records)
396
+ tracker = df.groupby(["Company", "Role"]).last().reset_index()
397
+ df.to_csv("/tmp/email_log.csv", index=False)
398
+ tracker.to_csv("/tmp/job_tracker.csv", index=False)
399
+
400
+ summary_html = make_summary_cards(tracker["Category"].value_counts())
401
+ log_html = make_log_html(log_items)
402
+ pipeline_html = make_pipeline_html(tracker)
403
+
404
+ yield (
405
+ gr.update(value=summary_html),
406
+ gr.update(value=log_html),
407
+ gr.update(value=pipeline_html),
408
+ "/tmp/email_log.csv",
409
+ "/tmp/job_tracker.csv",
410
+ )
411
+
412
+ # ──────────────────────────────────────────────
413
+ # GRADIO UI
414
+ # ──────────────────────────────────────────────
415
+ CSS = """
416
+ @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');
417
+
418
+ *, *::before, *::after { box-sizing: border-box; }
419
+
420
+ body, .gradio-container {
421
+ background: #010409 !important;
422
+ color: #e6edf3 !important;
423
+ font-family: 'DM Sans', sans-serif !important;
424
+ }
425
+
426
+ /* Tabs */
427
+ .tab-nav { background: #0d1117 !important; border-bottom: 1px solid #21262d !important; }
428
+ .tab-nav button { color: #8b949e !important; font-family: 'DM Sans', sans-serif !important; font-size:.85rem !important; }
429
+ .tab-nav button.selected { color: #58a6ff !important; border-bottom: 2px solid #58a6ff !important; background: transparent !important; }
430
+
431
+ /* Inputs */
432
+ label > span {
433
+ color: #8b949e !important;
434
+ font-size: .7rem !important;
435
+ letter-spacing: .12em !important;
436
+ text-transform: uppercase !important;
437
+ font-family: 'JetBrains Mono', monospace !important;
438
+ }
439
+ input[type=text], input[type=password], textarea {
440
+ background: #0d1117 !important;
441
+ border: 1px solid #30363d !important;
442
+ color: #e6edf3 !important;
443
+ border-radius: 6px !important;
444
+ font-family: 'DM Sans', sans-serif !important;
445
+ font-size: .88rem !important;
446
+ transition: border-color .2s !important;
447
+ }
448
+ input[type=text]:focus, input[type=password]:focus {
449
+ border-color: #58a6ff !important;
450
+ outline: none !important;
451
+ box-shadow: 0 0 0 3px #58a6ff18 !important;
452
+ }
453
+
454
+ /* Range slider */
455
+ input[type=range] { accent-color: #58a6ff !important; }
456
+
457
+ /* Buttons */
458
+ .gr-button {
459
+ font-family: 'DM Sans', sans-serif !important;
460
+ border-radius: 6px !important;
461
+ font-weight: 600 !important;
462
+ transition: all .2s !important;
463
+ }
464
+ .gr-button-primary {
465
+ background: #238636 !important;
466
+ border: 1px solid #2ea043 !important;
467
+ color: #ffffff !important;
468
+ }
469
+ .gr-button-primary:hover {
470
+ background: #2ea043 !important;
471
+ transform: translateY(-1px) !important;
472
+ box-shadow: 0 4px 16px #2ea04344 !important;
473
+ }
474
+ .gr-button-secondary {
475
+ background: #21262d !important;
476
+ border: 1px solid #30363d !important;
477
+ color: #c9d1d9 !important;
478
+ }
479
+
480
+ /* Panels */
481
+ .gr-panel, .gr-box, .gr-form {
482
+ background: #0d1117 !important;
483
+ border: 1px solid #21262d !important;
484
+ border-radius: 10px !important;
485
+ }
486
+
487
+ /* Dataframe */
488
+ .gr-dataframe table {
489
+ background: #0d1117 !important;
490
+ border: 1px solid #21262d !important;
491
+ font-family: 'JetBrains Mono', monospace !important;
492
+ font-size: .78rem !important;
493
+ }
494
+ .gr-dataframe th {
495
+ background: #161b22 !important;
496
+ color: #8b949e !important;
497
+ letter-spacing: .06em !important;
498
+ text-transform: uppercase !important;
499
+ }
500
+ .gr-dataframe td { color: #c9d1d9 !important; border-color: #21262d !important; }
501
+ .gr-dataframe tr:hover td { background: #161b22 !important; }
502
+
503
+ /* Scrollbar */
504
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
505
+ ::-webkit-scrollbar-track { background: #0d1117; }
506
+ ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
507
+ ::-webkit-scrollbar-thumb:hover { background: #484f58; }
508
+ """
509
+
510
+ HEADER_HTML = """
511
+ <div style="
512
+ padding: 32px 0 24px;
513
+ font-family: 'DM Sans', sans-serif;
514
+ border-bottom: 1px solid #21262d;
515
+ margin-bottom: 24px;
516
+ ">
517
+ <div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap;">
518
+ <div style="
519
+ background: linear-gradient(135deg, #238636, #3fb950);
520
+ border-radius: 12px;
521
+ padding: 12px 16px;
522
+ font-size: 1.6rem;
523
+ line-height:1;
524
+ ">πŸ“¬</div>
525
+ <div>
526
+ <div style="
527
+ font-size:.65rem; letter-spacing:.25em; text-transform:uppercase;
528
+ color:#58a6ff; font-family:'JetBrains Mono',monospace; margin-bottom:4px;
529
+ ">Powered by LLaMA-3 Β· Self-Learning Β· Groq</div>
530
+ <h1 style="
531
+ font-size:1.9rem; font-weight:800; color:#e6edf3;
532
+ margin:0; letter-spacing:-.03em; line-height:1;
533
+ ">Email Job Tracker</h1>
534
+ <p style="color:#8b949e; margin:6px 0 0; font-size:.88rem; font-weight:300;">
535
+ Fetch β†’ classify β†’ track your entire job pipeline automatically
536
+ </p>
537
+ </div>
538
+ <div style="margin-left:auto; text-align:right; display:flex; flex-direction:column; gap:6px;">
539
+ <span style="
540
+ background:#238636; color:#fff; font-size:.68rem;
541
+ padding:3px 10px; border-radius:20px; font-weight:600;
542
+ font-family:'JetBrains Mono',monospace; letter-spacing:.05em;
543
+ ">v2.0 GRADIO</span>
544
+ <span style="
545
+ background:#1f6feb22; color:#58a6ff; font-size:.68rem;
546
+ padding:3px 10px; border-radius:20px;
547
+ font-family:'JetBrains Mono',monospace; border:1px solid #1f6feb55;
548
+ ">HF SPACES</span>
549
+ </div>
550
+ </div>
551
+ </div>
552
+ """
553
+
554
+ CRED_HELP = """
555
+ <div style="
556
+ background: #161b22;
557
+ border: 1px solid #30363d;
558
+ border-left: 3px solid #58a6ff;
559
+ border-radius: 8px;
560
+ padding: 14px 16px;
561
+ font-size: .78rem;
562
+ color: #8b949e;
563
+ font-family: 'DM Sans', sans-serif;
564
+ line-height: 1.7;
565
+ ">
566
+ <strong style="color:#c9d1d9;">πŸ” Security Notes</strong><br>
567
+ β€’ Use a <strong style="color:#e6edf3;">Gmail App Password</strong>, not your account password<br>
568
+ β€’ Requires 2FA enabled β†’
569
+ <a href="https://myaccount.google.com/apppasswords" target="_blank" style="color:#58a6ff;">Generate here</a><br>
570
+ β€’ Get free Groq API key β†’
571
+ <a href="https://console.groq.com" target="_blank" style="color:#58a6ff;">console.groq.com</a><br>
572
+ β€’ Credentials are <strong style="color:#e6edf3;">never stored</strong> β€” session only
573
+ </div>
574
+ """
575
+
576
+ EMPTY_LOG = """
577
+ <div style="
578
+ background:#0d1117; border:1px solid #21262d; border-radius:10px;
579
+ padding:40px; text-align:center; color:#484f58;
580
+ font-family:'JetBrains Mono',monospace; font-size:.82rem;
581
+ ">
582
+ β–‘β–‘ No data yet β€” run the agent to see results β–‘β–‘
583
+ </div>"""
584
+
585
+ def section_label(text, color="#58a6ff"):
586
+ return f"""<div style="
587
+ font-size:.65rem; letter-spacing:.18em; text-transform:uppercase;
588
+ color:{color}; font-family:'JetBrains Mono',monospace;
589
+ margin-bottom:8px; margin-top:4px;
590
+ ">{text}</div>"""
591
+
592
+ with gr.Blocks(css=CSS, title="πŸ“¬ Email Job Tracker") as demo:
593
+ gr.HTML(HEADER_HTML)
594
+
595
+ with gr.Tabs():
596
+
597
+ # ════════════════════════════════════════
598
+ # TAB 1 β€” RUN AGENT
599
+ # ════════════════════════════════════════
600
+ with gr.TabItem("⚑ Run Agent"):
601
+ with gr.Row(equal_height=False):
602
+
603
+ # LEFT β€” credentials
604
+ with gr.Column(scale=1, min_width=300):
605
+ gr.HTML(section_label("Credentials"))
606
+ email_in = gr.Textbox(label="Gmail Address", placeholder="you@gmail.com")
607
+ pass_in = gr.Textbox(label="Gmail App Password", type="password", placeholder="xxxx xxxx xxxx xxxx")
608
+ groq_in = gr.Textbox(label="Groq API Key", type="password", placeholder="gsk_...")
609
+ gr.HTML(section_label("Settings", "#8b949e"))
610
+ limit_in = gr.Slider(label="Emails per folder", minimum=5, maximum=50, value=20, step=5)
611
+ run_btn = gr.Button("⚑ Run Agent", variant="primary", size="lg")
612
+ clear_btn = gr.Button("βœ• Clear Results", variant="secondary", size="sm")
613
+ gr.HTML(CRED_HELP)
614
+
615
+ # RIGHT β€” live results
616
+ with gr.Column(scale=2):
617
+ gr.HTML(section_label("Category Summary"))
618
+ summary_out = gr.HTML(value=EMPTY_LOG)
619
+
620
+ gr.HTML(section_label("Live Classification Log"))
621
+ log_out = gr.HTML(value=EMPTY_LOG)
622
+
623
+ gr.HTML(section_label("Job Pipeline View"))
624
+ pipeline_out = gr.HTML(value=EMPTY_LOG)
625
+
626
+ with gr.Row():
627
+ with gr.Column():
628
+ gr.HTML(section_label("Downloads", "#8b949e"))
629
+ file_log = gr.File(label="πŸ“„ email_log.csv β€” all classified emails")
630
+ file_tracker = gr.File(label="πŸ“Š job_tracker.csv β€” deduplicated pipeline")
631
+
632
+ # ════════════════════════════════════════
633
+ # TAB 2 β€” TRACKER TABLE
634
+ # ════════════════════════════════════════
635
+ with gr.TabItem("πŸ“Š Tracker Table"):
636
+ gr.HTML(section_label("Full Job Tracker β€” Latest status per company/role"))
637
+ table_out = gr.Dataframe(
638
+ headers=["Company", "Role", "Category", "Round", "Confidence", "Subject"],
639
+ interactive=False,
640
+ wrap=False,
641
+ )
642
+
643
+ # ════════════════════════════════════════
644
+ # TAB 3 β€” HOW TO USE
645
+ # ════════════════════════════════════════
646
+ with gr.TabItem("πŸ“– Guide"):
647
+ gr.HTML("""
648
+ <div style="max-width:680px; font-family:'DM Sans',sans-serif; line-height:1.8; padding:8px 0;">
649
+
650
+ <h2 style="color:#e6edf3; font-size:1.1rem; margin-bottom:4px;">πŸš€ Quick Start</h2>
651
+ <ol style="color:#8b949e; font-size:.88rem; padding-left:20px;">
652
+ <li>Enable 2-Step Verification on your Google Account</li>
653
+ <li>Generate a <a href="https://myaccount.google.com/apppasswords" target="_blank" style="color:#58a6ff;">Gmail App Password</a> (16 chars, no spaces)</li>
654
+ <li>Get a free API key from <a href="https://console.groq.com" target="_blank" style="color:#58a6ff;">Groq Console</a></li>
655
+ <li>Enter credentials in the <strong style="color:#c9d1d9;">Run Agent</strong> tab and hit ⚑ Run</li>
656
+ </ol>
657
+
658
+ <h2 style="color:#e6edf3; font-size:1.1rem; margin-top:20px; margin-bottom:4px;">🏷️ Email Categories</h2>
659
+ <table style="font-size:.82rem; border-collapse:collapse; width:100%;">
660
+ <thead>
661
+ <tr style="background:#161b22;">
662
+ <th style="padding:8px 12px; color:#8b949e; text-align:left; border:1px solid #21262d;">Category</th>
663
+ <th style="padding:8px 12px; color:#8b949e; text-align:left; border:1px solid #21262d;">Triggers</th>
664
+ </tr>
665
+ </thead>
666
+ <tbody>
667
+ <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</td></tr>
668
+ <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</td></tr>
669
+ <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</td></tr>
670
+ <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</td></tr>
671
+ <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</td></tr>
672
+ <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, Account Notification</td></tr>
673
+ <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, Subscribe</td></tr>
674
+ </tbody>
675
+ </table>
676
+
677
+ <h2 style="color:#e6edf3; font-size:1.1rem; margin-top:20px; margin-bottom:4px;">🧠 Self-Learning</h2>
678
+ <p style="color:#8b949e; font-size:.88rem;">
679
+ Every high-confidence classification (>80%) teaches the system new keywords for that category. These are saved to <code style="color:#58a6ff;">/tmp/learned_keywords.json</code> and improve accuracy over time within the session.
680
+ </p>
681
+
682
+ </div>
683
+ """)
684
+
685
+ # ════════════════════════════════════════
686
+ # EVENT HANDLERS
687
+ # ════════════════════════════════════════
688
+ def do_run(email, password, groq_key, limit, progress=gr.Progress(track_tqdm=True)):
689
+ tracker_df_holder = []
690
+ last_yield = None
691
+ for result in run_pipeline(email, password, groq_key, limit, progress):
692
+ last_yield = result
693
+ yield result[0], result[1], result[2], result[3], result[4], gr.update()
694
+
695
+ # populate tracker table after run
696
+ try:
697
+ df = pd.read_csv("/tmp/job_tracker.csv")
698
+ cols = [c for c in ["Company","Role","Category","Round","Confidence","Subject"] if c in df.columns]
699
+ yield last_yield[0], last_yield[1], last_yield[2], last_yield[3], last_yield[4], df[cols]
700
+ except Exception:
701
+ yield last_yield[0], last_yield[1], last_yield[2], last_yield[3], last_yield[4], gr.update()
702
+
703
+ def do_clear():
704
+ return EMPTY_LOG, EMPTY_LOG, EMPTY_LOG, None, None, None
705
+
706
+ run_btn.click(
707
+ fn=do_run,
708
+ inputs=[email_in, pass_in, groq_in, limit_in],
709
+ outputs=[summary_out, log_out, pipeline_out, file_log, file_tracker, table_out],
710
+ )
711
+ clear_btn.click(
712
+ fn=do_clear,
713
+ outputs=[summary_out, log_out, pipeline_out, file_log, file_tracker, table_out],
714
+ )
715
+
716
+ if __name__ == "__main__":
717
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ imapclient>=2.3.1
3
+ pyzmail36
4
+ openai>=1.0.0
5
+ pandas>=2.0.0