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

updated ui

Browse files
Files changed (1) hide show
  1. app.py +240 -238
app.py CHANGED
@@ -1,11 +1,9 @@
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
  """
@@ -16,21 +14,26 @@ 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 = {
@@ -44,15 +47,16 @@ CATEGORY_COLORS = {
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
  # ──────────────────────────────────────────────
@@ -139,7 +143,7 @@ Extract company name, role/position, and interview round if present.
139
  Email:
140
  {text}
141
 
142
- Return ONLY a valid JSON object β€” no markdown, no backticks:
143
  {{
144
  "category": "",
145
  "company": "",
@@ -152,6 +156,7 @@ Return ONLY a valid JSON object β€” no markdown, no backticks:
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()
@@ -166,32 +171,70 @@ Return ONLY a valid JSON object β€” no markdown, no backticks:
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
  # ──────────────────────────────────────────────
@@ -201,7 +244,7 @@ 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);
@@ -213,55 +256,51 @@ def make_summary_cards(counts):
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>
@@ -269,56 +308,48 @@ def make_log_html(log_items):
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
@@ -326,7 +357,7 @@ def make_pipeline_html(tracker_df):
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
@@ -335,7 +366,7 @@ def run_pipeline(email, password, groq_key, limit, progress=gr.Progress(track_tq
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
 
@@ -343,14 +374,14 @@ def run_pipeline(email, password, groq_key, limit, progress=gr.Progress(track_tq
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
@@ -380,37 +411,27 @@ def run_pipeline(email, password, groq_key, limit, progress=gr.Progress(track_tq
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');
@@ -423,12 +444,10 @@ body, .gradio-container {
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;
@@ -447,14 +466,10 @@ input[type=text], input[type=password], textarea {
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;
@@ -464,7 +479,8 @@ input[type=range] { accent-color: #58a6ff !important; }
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;
@@ -477,14 +493,12 @@ input[type=range] { accent-color: #58a6ff !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;
@@ -500,123 +514,106 @@ input[type=range] { accent-color: #58a6ff !important; }
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
 
@@ -626,76 +623,81 @@ with gr.Blocks(css=CSS, title="πŸ“¬ Email Job Tracker") as demo:
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()
 
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
  """
 
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",
34
  "[Gmail]/Starred",
35
+ "[Gmail]/Sent Mail",
36
+ "[Gmail]/Drafts",
37
  ]
38
 
39
  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
  # ──────────────────────────────────────────────
 
143
  Email:
144
  {text}
145
 
146
+ Return ONLY a valid JSON object, no markdown, no backticks:
147
  {{
148
  "category": "",
149
  "company": "",
 
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()
 
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")
206
+ elif msg.html_part:
207
+ body = msg.html_part.get_payload().decode(
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
  # ──────────────────────────────────────────────
 
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);
 
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>
 
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:
317
  return ""
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
 
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
 
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
 
 
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
 
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');
 
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;
 
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;
 
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;
 
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;
 
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>
551
  </div>
552
  </div>
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(css=CSS, 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
 
 
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()