Michtiii commited on
Commit
3c76aa3
Β·
verified Β·
1 Parent(s): e79078a

updated error

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