Vinit006 commited on
Commit
98d4f58
Β·
verified Β·
1 Parent(s): 81442df

Update app/gradio_ui.py

Browse files
Files changed (1) hide show
  1. app/gradio_ui.py +229 -391
app/gradio_ui.py CHANGED
@@ -1,251 +1,149 @@
 
 
 
 
 
1
  import uuid
2
  import requests
3
  import gradio as gr
4
 
5
- # ─── Config ─────────────────────────────────────────────────────
6
  BASE_URL = "https://vinit006-emailagentwithmemory.hf.space"
7
 
8
-
9
- # ─── Theme: dark navy / electric-blue, matching the HTML frontend ─
10
- THEME_CSS = """
11
- :root {
12
- --bg: #0D1117;
13
- --surface: #161B22;
14
- --surface2: #1C2333;
15
- --border: #30363D;
16
- --accent: #58A6FF;
17
- --accent-dim:#1a2738;
18
- --success: #3FB950;
19
- --success-dim: #122a18;
20
- --warning: #D29922;
21
- --warning-dim: #2c2310;
22
- --danger: #F85149;
23
- --danger-dim: #2c1414;
24
- --text: #E6EDF3;
25
- --text-muted:#8B949E;
26
- --text-dim: #6E7681;
27
- }
28
-
29
- .gradio-container {
30
- background: var(--bg) !important;
31
- color: var(--text) !important;
32
- font-family: 'Inter', -apple-system, sans-serif !important;
33
- }
34
-
35
- /* Headings / markdown */
36
- .gradio-container h1, .gradio-container h2, .gradio-container h3 {
37
- color: var(--text) !important;
38
- letter-spacing: -0.02em;
39
- }
40
- .gradio-container p, .gradio-container label span {
41
- color: var(--text-muted) !important;
42
- }
43
-
44
- /* Cards / blocks */
45
- .gr-block, .gr-box, .block {
46
- background: var(--surface) !important;
47
- border: 1px solid var(--border) !important;
48
- border-radius: 12px !important;
49
- }
50
-
51
- /* Inputs */
52
- textarea, input[type="text"], input[type="email"] {
53
- background: var(--bg) !important;
54
- border: 1px solid var(--border) !important;
55
- color: var(--text) !important;
56
- border-radius: 8px !important;
57
- font-family: 'JetBrains Mono', monospace !important;
58
- font-size: 13px !important;
59
- }
60
- textarea:focus, input:focus {
61
- border-color: var(--accent) !important;
62
- box-shadow: 0 0 0 3px rgba(88,166,255,0.1) !important;
63
- }
64
-
65
- /* Buttons */
66
- button {
67
- border-radius: 8px !important;
68
- font-weight: 500 !important;
69
- border: none !important;
70
- transition: all 0.15s !important;
71
- }
72
- .primary, button.primary {
73
- background: var(--accent) !important;
74
- color: #0D1117 !important;
75
- }
76
- .primary:hover { background: #79BAFF !important; }
77
-
78
- #approve-btn button, #approve-btn {
79
- background: var(--success) !important;
80
- color: #0D1117 !important;
81
- }
82
- #approve-btn button:hover { background: #56D364 !important; }
83
-
84
- #reject-btn button, #reject-btn {
85
- background: var(--danger) !important;
86
- color: #fff !important;
87
- }
88
- #reject-btn button:hover { background: #FF7B72 !important; }
89
-
90
- #changes-btn button, #changes-btn {
91
- background: var(--warning) !important;
92
- color: #0D1117 !important;
93
  }
94
- #changes-btn button:hover { background: #E3AD40 !important; }
95
 
96
- #send-btn button {
97
- background: var(--success) !important;
98
- color: #0D1117 !important;
99
- }
100
-
101
- /* Pipeline status pills */
102
- .pipeline-pill {
103
- display: inline-block;
104
- padding: 4px 12px;
105
- border-radius: 20px;
106
- font-family: 'JetBrains Mono', monospace;
107
- font-size: 11px;
108
- font-weight: 600;
109
- letter-spacing: 0.05em;
110
- margin-right: 6px;
111
- border: 1px solid var(--border);
112
- color: var(--text-dim);
113
- background: var(--surface2);
114
- }
115
- .pipeline-pill.active {
116
- border-color: var(--accent);
117
- color: var(--accent);
118
- background: var(--accent-dim);
119
- }
120
- .pipeline-pill.done {
121
- border-color: var(--success);
122
- color: var(--success);
123
- background: var(--success-dim);
124
- }
125
-
126
- /* Triage badge */
127
- .triage-badge {
128
- display: inline-block;
129
- font-family: 'JetBrains Mono', monospace;
130
- font-size: 11px;
131
- font-weight: 700;
132
- letter-spacing: 0.05em;
133
- padding: 4px 10px;
134
- border-radius: 6px;
135
- margin-top: 6px;
136
- }
137
- .triage-follow-up {
138
- background: var(--warning-dim);
139
- color: var(--warning);
140
- border: 1px solid var(--warning);
141
- }
142
- .triage-no-action {
143
- background: var(--success-dim);
144
- color: var(--success);
145
- border: 1px solid var(--success);
146
- }
147
- .triage-other {
148
- background: var(--accent-dim);
149
- color: var(--accent);
150
- border: 1px solid var(--accent);
151
- }
152
-
153
- /* Draft display card */
154
- .draft-card {
155
- background: var(--surface) !important;
156
- border: 1px solid var(--border) !important;
157
- border-radius: 12px !important;
158
- padding: 18px !important;
159
- }
160
- .draft-meta-label {
161
- font-family: 'JetBrains Mono', monospace;
162
- font-size: 10px;
163
- letter-spacing: 0.1em;
164
- color: var(--text-dim);
165
- text-transform: uppercase;
166
- }
167
-
168
- /* Status / error text */
169
- .status-ok { color: var(--success) !important; }
170
- .status-err { color: var(--danger) !important; }
171
- """
172
-
173
-
174
- # ─── Helpers ────────────────────────────────────────────────────
175
- def _gen_thread_id() -> str:
176
- return str(uuid.uuid4())[:8]
177
 
178
 
179
  def _pipeline_html(active_step: str, done_steps=None) -> str:
180
- """Render the TRIAGE β†’ DRAFT β†’ REVIEW β†’ SEND pill strip."""
181
  done_steps = done_steps or []
182
  steps = ["TRIAGE", "DRAFT", "REVIEW", "SEND"]
183
- html = ""
184
  for s in steps:
185
- cls = "pipeline-pill"
186
  if s == active_step:
187
- cls += " active"
188
  elif s in done_steps:
189
- cls += " done"
190
- html += f'<span class="{cls}">{s}</span>'
191
- return html
 
192
 
193
 
194
  def _triage_badge_html(label: str) -> str:
195
  if not label:
196
  return ""
197
- cls = "triage-other"
198
  if label == "FOLLOW_UP_REQUIRED":
199
- cls = "triage-follow-up"
200
  elif label == "NO_ACTION":
201
- cls = "triage-no-action"
202
- return f'<span class="triage-badge {cls}">{label}</span>'
 
 
 
 
 
 
 
 
 
203
 
204
 
205
  def _draft_card_html(to, subject, body) -> str:
206
- to = to or "β€”"
207
  subject = subject or "β€”"
208
- body = (body or "β€”").replace("\n", "<br>")
209
  return f"""
210
- <div class="draft-card">
211
- <div style="display:flex; gap:32px; margin-bottom:14px;">
 
212
  <div>
213
- <div class="draft-meta-label">TO</div>
214
- <div style="font-family:'JetBrains Mono',monospace; font-size:13px;">{to}</div>
 
215
  </div>
216
  <div>
217
- <div class="draft-meta-label">SUBJECT</div>
218
- <div style="font-size:13px;">{subject}</div>
 
219
  </div>
220
  </div>
221
- <div style="height:1px; background:var(--border); margin-bottom:14px;"></div>
222
- <div style="font-size:13px; line-height:1.7; color:var(--text-muted);">{body}</div>
 
 
 
 
 
 
 
 
 
 
 
 
223
  </div>
224
  """
225
 
226
 
227
- # ─── API calls (talk to this same backend's endpoints) ──────────
228
- def api_get_user_data(user_email: str):
 
 
 
 
 
 
 
 
 
229
  res = requests.post(f"{BASE_URL}/get-user-data", params={"user_email": user_email})
230
  res.raise_for_status()
231
  return res.json()
232
 
233
-
234
  def api_process_email(token, thread_id, sender_email, subject, body):
235
  res = requests.post(
236
  f"{BASE_URL}/process-email",
237
  headers={"Authorization": f"Bearer {token}"},
238
- json={
239
- "thread_id": thread_id,
240
- "sender_email_id": sender_email,
241
- "sender_subject": subject,
242
- "sender_email_body": body,
243
- },
244
  )
245
  res.raise_for_status()
246
  return res.json()
247
 
248
-
249
  def api_review_action(token, thread_id, status, feedback=None):
250
  payload = {"thread_id": thread_id, "status": status}
251
  if feedback:
@@ -258,13 +156,7 @@ def api_review_action(token, thread_id, status, feedback=None):
258
  res.raise_for_status()
259
  return res.json()
260
 
261
-
262
  def api_send_email(token, thread_id, command_text):
263
- """
264
- `command_text` is a short instruction for the agent about WHEN/HOW
265
- to send the already-approved draft β€” e.g. "send now", "send later
266
- today", "hold until tomorrow morning". It does NOT edit draft content.
267
- """
268
  res = requests.post(
269
  f"{BASE_URL}/send_email",
270
  headers={"Authorization": f"Bearer {token}"},
@@ -274,148 +166,133 @@ def api_send_email(token, thread_id, command_text):
274
  return res.json()
275
 
276
 
277
- # ─── Gradio App ─────────────────────────────────────────────────
278
  def build_demo() -> gr.Blocks:
279
- with gr.Blocks(css=THEME_CSS, title="Email Agent β€” AI Triage & Reply") as demo:
280
 
281
- # ─── Session state ──────────────────────────────────────
282
- token_state = gr.State(None)
283
- thread_state = gr.State(_gen_thread_id())
284
- triage_label_state = gr.State(None)
285
 
286
  gr.Markdown("# ⚑ Email Agent\n_AI-powered triage, drafting & sending β€” with a human in the loop._")
287
 
288
- # ─── Auth row ────────────────────────────────────────────
289
  with gr.Group(visible=True) as auth_group:
290
  gr.Markdown("### Sign In")
291
  with gr.Row():
292
  email_input = gr.Textbox(label="Your Email", placeholder="you@example.com", scale=3)
293
- login_btn = gr.Button("Sign In β†’", variant="primary", scale=1)
294
- auth_status = gr.Markdown("")
295
 
296
- # ─── Main workspace (hidden until signed in) ─────────────
297
  with gr.Group(visible=False) as workspace_group:
 
 
298
  with gr.Row():
299
- user_pill = gr.Markdown("")
300
  with gr.Row():
301
- thread_display = gr.Textbox(label="Thread ID", interactive=True, scale=3)
302
- new_thread_btn = gr.Button("β†Ί New Thread", scale=1)
303
-
304
- pipeline_html = gr.HTML(_pipeline_html("TRIAGE"))
305
- triage_badge_html = gr.HTML("")
306
 
 
 
307
  gr.Markdown("---")
308
 
309
- # ─── Step 1: Process Email ────────────────────────────
310
  with gr.Group(visible=True) as process_group:
311
  gr.Markdown("### πŸ“₯ Incoming Email")
312
  with gr.Row():
313
- sender_email = gr.Textbox(label="From (sender email)", placeholder="sender@company.com")
314
  sender_subject = gr.Textbox(label="Subject", placeholder="Email subject line")
315
- sender_body = gr.Textbox(label="Email Body", lines=8, placeholder="Paste the full email body here…")
316
- process_btn = gr.Button("⚑ Process Email", variant="primary")
317
- process_status = gr.Markdown("")
 
318
 
319
- # ─── Step 2: Review Draft ──────────────────────────────
320
  with gr.Group(visible=False) as review_group:
321
- gr.Markdown("### ✍️ Review Draft\nThe agent has drafted a reply. Approve, request changes, or reject.")
322
  draft_html = gr.HTML("")
323
-
324
  with gr.Row():
325
- approve_btn = gr.Button("βœ“ Approve & Finalize Draft", elem_id="approve-btn")
326
- changes_btn = gr.Button("β†Ί Request Changes", elem_id="changes-btn")
327
- reject_btn = gr.Button("βœ• Reject", elem_id="reject-btn")
328
-
329
  with gr.Group(visible=False) as feedback_group:
330
- feedback_text = gr.Textbox(
331
- label="Feedback for agent",
332
- lines=4,
333
- placeholder="Tell the agent what to change in the draft…",
334
- )
335
- submit_feedback_btn = gr.Button("Submit Feedback & Redraft", elem_id="changes-btn")
336
 
337
- review_status = gr.Markdown("")
338
-
339
- # ─── Step 3: Send Email ────────────────────────────────
340
  with gr.Group(visible=False) as send_group:
341
  gr.Markdown(
342
- "### πŸ“¨ Send Email\n"
343
- "Draft approved. The agent can only **draft** and **send** β€” "
344
- "it can't edit content here. Tell it **when/how to send** "
345
- "(e.g. *\"send now\"*, *\"send later today\"*, *\"hold off till tomorrow\"*)."
346
  )
347
  send_command = gr.Textbox(
348
  label="Send Command",
349
- placeholder='e.g. "send now" / "send later" / "hold off until tomorrow"',
350
  )
351
- send_btn = gr.Button("πŸ“¨ Send Email", elem_id="send-btn", variant="primary")
352
- send_status = gr.Markdown("")
353
 
354
- # ─── Step 4: Result ─────────────────────────────────────
355
  with gr.Group(visible=False) as result_group:
356
- result_html = gr.HTML("")
357
  new_email_btn = gr.Button("Process Another Email", variant="primary")
358
 
359
- # ══════════════════════════════════════════════════════════
360
- # Callbacks
361
- # ══════════════════════════════════════════════════════════
 
 
 
362
 
 
363
  def do_login(email):
364
  if not email or "@" not in email:
365
- return (
366
- gr.update(), gr.update(), None, None,
367
- "<span class='status-err'>Enter a valid email address.</span>",
368
- gr.update(), "",
369
- )
370
  try:
371
- data = api_get_user_data(email)
372
  token = data["token"]
373
- pill_md = f"**Signed in as:** `{data['email']}`"
374
- return (
375
- gr.update(visible=False), # auth_group
376
- gr.update(visible=True), # workspace_group
377
- token, # token_state
378
- data["user_id"], # (kept in case needed)
379
- "", # auth_status
380
- gr.update(), # placeholder
381
- pill_md, # user_pill
382
  )
 
 
383
  except Exception as e:
384
- return (
385
- gr.update(), gr.update(), None, None,
386
- f"<span class='status-err'>Sign-in failed: {e}</span>",
387
- gr.update(), "",
388
- )
389
 
390
  login_btn.click(
391
- do_login,
392
- inputs=[email_input],
393
- outputs=[auth_group, workspace_group, token_state, gr.State(), auth_status, gr.State(), user_pill],
394
  )
395
 
 
396
  def new_thread():
397
- tid = _gen_thread_id()
398
  return tid, tid
399
 
400
  new_thread_btn.click(new_thread, outputs=[thread_state, thread_display])
 
401
 
402
- def sync_thread_id(tid):
403
- return tid
404
-
405
- thread_display.change(sync_thread_id, inputs=[thread_display], outputs=[thread_state])
406
-
407
- # ─── Process Email ──────────────────────────────────────
408
  def do_process(token, thread_id, s_email, s_subject, s_body):
409
  if not (s_email and s_subject and s_body):
410
  return (
411
- gr.update(), gr.update(), gr.update(), None,
412
- "<span class='status-err'>Fill in sender email, subject, and body.</span>",
413
- _pipeline_html("TRIAGE"), "",
414
- gr.update(), gr.update(),
415
  )
416
  try:
417
- data = api_process_email(token, thread_id, s_email, s_subject, s_body)
418
- label = data.get("triage_label")
419
  badge = _triage_badge_html(label)
420
 
421
  if data.get("status") == "needs_review":
@@ -424,175 +301,136 @@ def build_demo() -> gr.Blocks:
424
  gr.update(visible=False), # process_group
425
  gr.update(visible=True), # review_group
426
  gr.update(visible=False), # send_group
427
- label, # triage_label_state
428
- "", # process_status
429
  _pipeline_html("REVIEW", ["TRIAGE", "DRAFT"]),
430
  badge,
431
  _draft_card_html(draft.get("to"), draft.get("subject"), draft.get("body")),
432
- gr.update(visible=False), # result_group
433
  )
434
  else:
435
- # No action needed β€” triage only, nothing to review/send
436
- detail = "No reply needed β€” the agent filed this email." if label == "NO_ACTION" else f"Triage complete. Label: {label}"
437
- result = f"""
438
- <div class="draft-card" style="text-align:center;">
439
- <div style="font-size:32px; margin-bottom:8px;">{'βœ…' if label == 'NO_ACTION' else 'πŸ“‹'}</div>
440
- <div style="font-size:16px; font-weight:600; margin-bottom:6px;">Triage Complete</div>
441
- <div style="color:var(--text-muted); font-size:13px; margin-bottom:10px;">{detail}</div>
442
- {badge}
443
- </div>
444
- """
445
  return (
446
  gr.update(visible=False),
447
  gr.update(visible=False),
448
  gr.update(visible=False),
449
- label,
450
  "",
451
- _pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW"]),
452
  badge,
453
  "",
454
- gr.update(visible=True, value=result),
455
  )
456
  except Exception as e:
457
  return (
458
- gr.update(), gr.update(), gr.update(), None,
459
- f"<span class='status-err'>Error processing email: {e}</span>",
460
- _pipeline_html("TRIAGE"), "", gr.update(), gr.update(),
461
  )
462
 
463
  process_btn.click(
464
  do_process,
465
  inputs=[token_state, thread_state, sender_email, sender_subject, sender_body],
466
- outputs=[
467
- process_group, review_group, send_group, triage_label_state,
468
- process_status, pipeline_html, triage_badge_html, draft_html, result_group,
469
- ],
470
  )
471
 
472
- # ─── Review: toggle feedback box ────────────────────────
473
- def toggle_feedback():
474
- return gr.update(visible=True)
475
-
476
- changes_btn.click(toggle_feedback, outputs=[feedback_group])
477
 
478
- # ─── Review: approve / reject / submit feedback ─────────
479
  def do_review(token, thread_id, status, feedback):
480
  try:
481
  data = api_review_action(token, thread_id, status, feedback)
482
-
483
  if data.get("status") == "needs_review":
484
- # Redrafted β€” show new draft, stay on review step
485
  draft = data.get("email_draft", {})
486
  return (
487
- gr.update(visible=True), # review_group
488
- gr.update(visible=False), # send_group
489
  _draft_card_html(draft.get("to"), draft.get("subject"), draft.get("body")),
490
- gr.update(visible=False), # feedback_group (collapse after redraft)
491
- "", # feedback_text clear
492
- "<span class='status-ok'>Agent redrafted β€” review the new version.</span>",
493
  _pipeline_html("REVIEW", ["TRIAGE", "DRAFT"]),
494
  )
495
  elif data.get("draft_id"):
496
  return (
497
- gr.update(visible=False), # review_group
498
- gr.update(visible=True), # send_group
499
- gr.update(),
500
- gr.update(visible=False),
501
- "",
502
- "<span class='status-ok'>Draft approved! Ready to send.</span>",
503
  _pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW"]),
504
  )
505
  else:
506
  return (
507
- gr.update(visible=False), gr.update(visible=True), gr.update(),
508
- gr.update(visible=False), "", "", _pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW"]),
 
509
  )
510
  except Exception as e:
511
  return (
512
  gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
513
- f"<span class='status-err'>Review action failed: {e}</span>", gr.update(),
514
  )
515
 
516
- approve_btn.click(
517
- lambda token, tid: do_review(token, tid, "approved", None),
518
- inputs=[token_state, thread_state],
519
- outputs=[review_group, send_group, draft_html, feedback_group, feedback_text, review_status, pipeline_html],
520
- )
521
-
522
- reject_btn.click(
523
- lambda token, tid: do_review(token, tid, "rejected", None),
524
- inputs=[token_state, thread_state],
525
- outputs=[review_group, send_group, draft_html, feedback_group, feedback_text, review_status, pipeline_html],
526
- )
527
 
528
- submit_feedback_btn.click(
529
- lambda token, tid, fb: do_review(token, tid, "rejected", fb),
530
- inputs=[token_state, thread_state, feedback_text],
531
- outputs=[review_group, send_group, draft_html, feedback_group, feedback_text, review_status, pipeline_html],
532
- )
 
 
533
 
534
- # ─── Send Email ──────────────────────────────────────────
535
  def do_send(token, thread_id, command_text):
536
  try:
537
- data = api_send_email(token, thread_id, command_text)
538
- msg_id = data.get("sent_message_id")
539
- detail = f"Message ID: <code>{msg_id}</code>" if msg_id else "Email dispatched."
540
- result = f"""
541
- <div class="draft-card" style="text-align:center;">
542
- <div style="font-size:32px; margin-bottom:8px;">πŸ“¨</div>
543
- <div style="font-size:16px; font-weight:600; margin-bottom:6px;">Email Sent</div>
544
- <div style="color:var(--text-muted); font-size:12px; font-family:'JetBrains Mono',monospace;">{detail}</div>
545
- </div>
546
- """
547
  return (
548
- gr.update(visible=False), # send_group
549
- gr.update(visible=True, value=result), # result_group
550
  "",
551
  _pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW", "SEND"]),
552
  )
553
  except Exception as e:
554
  return (
555
  gr.update(), gr.update(),
556
- f"<span class='status-err'>Failed to send: {e}</span>", gr.update(),
557
  )
558
 
559
  send_btn.click(
560
  do_send,
561
  inputs=[token_state, thread_state, send_command],
562
- outputs=[send_group, result_group, send_status, pipeline_html],
563
  )
564
 
565
- # ─── Start New ───────────────────────────────────────────
566
  def start_new():
567
- new_tid = _gen_thread_id()
568
  return (
569
- new_tid, new_tid,
570
- gr.update(visible=True), # process_group
571
- gr.update(visible=False), # review_group
572
- gr.update(visible=False), # send_group
573
- gr.update(visible=False), # result_group
574
- "", "", "", # clear sender fields
575
- "", "", # clear send command, draft html
576
- gr.update(visible=False), "", # feedback group + text
577
  _pipeline_html("TRIAGE"), "",
578
  )
579
 
580
  new_email_btn.click(
581
  start_new,
582
- outputs=[
583
- thread_state, thread_display,
584
- process_group, review_group, send_group, result_group,
585
- sender_email, sender_subject, sender_body,
586
- send_command, draft_html,
587
- feedback_group, feedback_text,
588
- pipeline_html, triage_badge_html,
589
- ],
590
  )
591
 
592
- return demo
593
-
594
-
595
-
596
-
597
-
598
-
 
1
+ """
2
+ gradio_ui.py β€” inline-styles only, no CSS class dependencies
3
+ Works reliably on HF Spaces where Gradio scopes/blocks external CSS.
4
+ """
5
+
6
  import uuid
7
  import requests
8
  import gradio as gr
9
 
 
10
  BASE_URL = "https://vinit006-emailagentwithmemory.hf.space"
11
 
12
+ # Colors as Python constants β€” used in inline styles throughout
13
+ C = {
14
+ "bg": "#0D1117",
15
+ "surface": "#161B22",
16
+ "surface2": "#1C2333",
17
+ "border": "#30363D",
18
+ "accent": "#58A6FF",
19
+ "accent_dim": "#1a2738",
20
+ "success": "#3FB950",
21
+ "success_dim": "#122a18",
22
+ "warning": "#D29922",
23
+ "warning_dim": "#2c2310",
24
+ "danger": "#F85149",
25
+ "danger_dim": "#2c1414",
26
+ "text": "#E6EDF3",
27
+ "muted": "#8B949E",
28
+ "dim": "#6E7681",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
 
30
 
31
+ # ─── HTML builders (all inline styles) ──────────────────────────
32
+
33
+ def _pill(label: str, state: str) -> str:
34
+ """state: 'idle' | 'active' | 'done'"""
35
+ if state == "active":
36
+ bg, border, color = C["accent_dim"], C["accent"], C["accent"]
37
+ elif state == "done":
38
+ bg, border, color = C["success_dim"], C["success"], C["success"]
39
+ else:
40
+ bg, border, color = C["surface2"], C["border"], C["dim"]
41
+
42
+ return (
43
+ f'<span style="'
44
+ f'display:inline-block;padding:4px 14px;border-radius:20px;'
45
+ f'font-family:monospace;font-size:11px;font-weight:700;'
46
+ f'letter-spacing:0.06em;margin-right:8px;'
47
+ f'border:1px solid {border};color:{color};background:{bg};">'
48
+ f'{label}</span>'
49
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
 
52
  def _pipeline_html(active_step: str, done_steps=None) -> str:
 
53
  done_steps = done_steps or []
54
  steps = ["TRIAGE", "DRAFT", "REVIEW", "SEND"]
55
+ pills = ""
56
  for s in steps:
 
57
  if s == active_step:
58
+ pills += _pill(s, "active")
59
  elif s in done_steps:
60
+ pills += _pill(s, "done")
61
+ else:
62
+ pills += _pill(s, "idle")
63
+ return f'<div style="padding:10px 0 6px;">{pills}</div>'
64
 
65
 
66
  def _triage_badge_html(label: str) -> str:
67
  if not label:
68
  return ""
 
69
  if label == "FOLLOW_UP_REQUIRED":
70
+ bg, border, color = C["warning_dim"], C["warning"], C["warning"]
71
  elif label == "NO_ACTION":
72
+ bg, border, color = C["success_dim"], C["success"], C["success"]
73
+ else:
74
+ bg, border, color = C["accent_dim"], C["accent"], C["accent"]
75
+
76
+ return (
77
+ f'<div style="margin-top:6px;">'
78
+ f'<span style="display:inline-block;font-family:monospace;font-size:12px;'
79
+ f'font-weight:700;letter-spacing:0.05em;padding:5px 14px;border-radius:6px;'
80
+ f'background:{bg};color:{color};border:1px solid {border};">'
81
+ f'{label}</span></div>'
82
+ )
83
 
84
 
85
  def _draft_card_html(to, subject, body) -> str:
86
+ to = to or "β€”"
87
  subject = subject or "β€”"
88
+ body = (body or "β€”").replace("\n", "<br>")
89
  return f"""
90
+ <div style="background:{C['surface']};border:1px solid {C['border']};
91
+ border-radius:12px;padding:20px;margin-top:8px;">
92
+ <div style="display:flex;gap:40px;margin-bottom:14px;flex-wrap:wrap;">
93
  <div>
94
+ <div style="font-family:monospace;font-size:10px;letter-spacing:0.1em;
95
+ color:{C['dim']};text-transform:uppercase;margin-bottom:4px;">TO</div>
96
+ <div style="font-family:monospace;font-size:13px;color:{C['text']};">{to}</div>
97
  </div>
98
  <div>
99
+ <div style="font-family:monospace;font-size:10px;letter-spacing:0.1em;
100
+ color:{C['dim']};text-transform:uppercase;margin-bottom:4px;">SUBJECT</div>
101
+ <div style="font-size:13px;color:{C['text']};">{subject}</div>
102
  </div>
103
  </div>
104
+ <div style="height:1px;background:{C['border']};margin-bottom:14px;"></div>
105
+ <div style="font-size:13px;line-height:1.75;color:{C['muted']};">{body}</div>
106
+ </div>
107
+ """
108
+
109
+
110
+ def _result_card_html(icon: str, title: str, detail: str, badge_html: str = "") -> str:
111
+ return f"""
112
+ <div style="background:{C['surface']};border:1px solid {C['border']};
113
+ border-radius:12px;padding:40px 32px;text-align:center;margin-top:8px;">
114
+ <div style="font-size:44px;margin-bottom:14px;">{icon}</div>
115
+ <div style="font-size:18px;font-weight:600;color:{C['text']};margin-bottom:8px;">{title}</div>
116
+ <div style="font-size:13px;color:{C['muted']};margin-bottom:12px;">{detail}</div>
117
+ {badge_html}
118
  </div>
119
  """
120
 
121
 
122
+ def _status_html(msg: str, ok: bool) -> str:
123
+ color = C["success"] if ok else C["danger"]
124
+ icon = "βœ“" if ok else "βœ•"
125
+ return (
126
+ f'<div style="margin-top:6px;font-size:13px;color:{color};font-weight:500;">'
127
+ f'{icon} {msg}</div>'
128
+ )
129
+
130
+
131
+ # ─── API calls ──────────────────────────────────────────────────
132
+ def api_get_user_data(user_email):
133
  res = requests.post(f"{BASE_URL}/get-user-data", params={"user_email": user_email})
134
  res.raise_for_status()
135
  return res.json()
136
 
 
137
  def api_process_email(token, thread_id, sender_email, subject, body):
138
  res = requests.post(
139
  f"{BASE_URL}/process-email",
140
  headers={"Authorization": f"Bearer {token}"},
141
+ json={"thread_id": thread_id, "sender_email_id": sender_email,
142
+ "sender_subject": subject, "sender_email_body": body},
 
 
 
 
143
  )
144
  res.raise_for_status()
145
  return res.json()
146
 
 
147
  def api_review_action(token, thread_id, status, feedback=None):
148
  payload = {"thread_id": thread_id, "status": status}
149
  if feedback:
 
156
  res.raise_for_status()
157
  return res.json()
158
 
 
159
  def api_send_email(token, thread_id, command_text):
 
 
 
 
 
160
  res = requests.post(
161
  f"{BASE_URL}/send_email",
162
  headers={"Authorization": f"Bearer {token}"},
 
166
  return res.json()
167
 
168
 
169
+ # ─── Build UI ────────────────────────────────────────────────────
170
  def build_demo() -> gr.Blocks:
171
+ with gr.Blocks(title="Email Agent β€” AI Triage & Reply") as demo:
172
 
173
+ token_state = gr.State(None)
174
+ thread_state = gr.State(None)
 
 
175
 
176
  gr.Markdown("# ⚑ Email Agent\n_AI-powered triage, drafting & sending β€” with a human in the loop._")
177
 
178
+ # ── Auth ─────────────────────────────────────────────────
179
  with gr.Group(visible=True) as auth_group:
180
  gr.Markdown("### Sign In")
181
  with gr.Row():
182
  email_input = gr.Textbox(label="Your Email", placeholder="you@example.com", scale=3)
183
+ login_btn = gr.Button("Sign In β†’", variant="primary", scale=1)
184
+ auth_status = gr.HTML("")
185
 
186
+ # ── Workspace ────────────────────────────────────────────
187
  with gr.Group(visible=False) as workspace_group:
188
+
189
+ # Session bar
190
  with gr.Row():
191
+ user_pill = gr.HTML("")
192
  with gr.Row():
193
+ thread_display = gr.Textbox(label="Thread ID", interactive=True, scale=3)
194
+ new_thread_btn = gr.Button("β†Ί New Thread", scale=1)
 
 
 
195
 
196
+ pipeline_disp = gr.HTML(_pipeline_html("TRIAGE"))
197
+ triage_badge_disp = gr.HTML("")
198
  gr.Markdown("---")
199
 
200
+ # Step 1
201
  with gr.Group(visible=True) as process_group:
202
  gr.Markdown("### πŸ“₯ Incoming Email")
203
  with gr.Row():
204
+ sender_email = gr.Textbox(label="From (sender email)", placeholder="sender@company.com")
205
  sender_subject = gr.Textbox(label="Subject", placeholder="Email subject line")
206
+ sender_body = gr.Textbox(label="Email Body", lines=8,
207
+ placeholder="Paste the full email body here…")
208
+ process_btn = gr.Button("⚑ Process Email", variant="primary")
209
+ process_status = gr.HTML("")
210
 
211
+ # Step 2
212
  with gr.Group(visible=False) as review_group:
213
+ gr.Markdown("### ✍️ Review Draft")
214
  draft_html = gr.HTML("")
 
215
  with gr.Row():
216
+ approve_btn = gr.Button("βœ“ Approve & Finalize", variant="primary")
217
+ changes_btn = gr.Button("β†Ί Request Changes")
218
+ reject_btn = gr.Button("βœ• Reject")
 
219
  with gr.Group(visible=False) as feedback_group:
220
+ feedback_text = gr.Textbox(label="Feedback for agent", lines=4,
221
+ placeholder="Tell the agent what to change…")
222
+ submit_feedback_btn = gr.Button("Submit Feedback & Redraft")
223
+ review_status = gr.HTML("")
 
 
224
 
225
+ # Step 3
 
 
226
  with gr.Group(visible=False) as send_group:
227
  gr.Markdown(
228
+ "### πŸ“¨ Send Command\n"
229
+ "Draft saved in Gmail. Give the agent a send command.\n\n"
230
+ "> ℹ️ The agent can only **draft** and **send** β€” "
231
+ "it cannot edit content here. This is a timing command only."
232
  )
233
  send_command = gr.Textbox(
234
  label="Send Command",
235
+ placeholder='"send now" / "send later today" / "hold off until tomorrow"',
236
  )
237
+ send_btn = gr.Button("πŸ“¨ Execute Send Command", variant="primary")
238
+ send_status = gr.HTML("")
239
 
240
+ # Step 4
241
  with gr.Group(visible=False) as result_group:
242
+ result_html = gr.HTML("")
243
  new_email_btn = gr.Button("Process Another Email", variant="primary")
244
 
245
+ # ── on_load: fresh thread ID per browser session ─────────
246
+ def on_load():
247
+ tid = str(uuid.uuid4())[:8]
248
+ return tid, tid
249
+
250
+ demo.load(on_load, outputs=[thread_state, thread_display])
251
 
252
+ # ── Login ─────────────────────────────────────────────────
253
  def do_login(email):
254
  if not email or "@" not in email:
255
+ return (gr.update(), gr.update(), None,
256
+ _status_html("Enter a valid email address.", False), "")
 
 
 
257
  try:
258
+ data = api_get_user_data(email)
259
  token = data["token"]
260
+ pill = (
261
+ f'<div style="background:{C["accent_dim"]};border:1px solid {C["accent"]};'
262
+ f'border-radius:20px;padding:5px 14px;display:inline-block;'
263
+ f'font-family:monospace;font-size:12px;color:{C["accent"]};">'
264
+ f'Signed in as: {data["email"]}</div>'
 
 
 
 
265
  )
266
+ return (gr.update(visible=False), gr.update(visible=True),
267
+ token, "", pill)
268
  except Exception as e:
269
+ return (gr.update(), gr.update(), None,
270
+ _status_html(f"Sign-in failed: {e}", False), "")
 
 
 
271
 
272
  login_btn.click(
273
+ do_login, inputs=[email_input],
274
+ outputs=[auth_group, workspace_group, token_state, auth_status, user_pill],
 
275
  )
276
 
277
+ # ── Thread ────────────────────────────────────────────────
278
  def new_thread():
279
+ tid = str(uuid.uuid4())[:8]
280
  return tid, tid
281
 
282
  new_thread_btn.click(new_thread, outputs=[thread_state, thread_display])
283
+ thread_display.change(lambda v: v, inputs=[thread_display], outputs=[thread_state])
284
 
285
+ # ── Process Email ─────────────────────────────────────────
 
 
 
 
 
286
  def do_process(token, thread_id, s_email, s_subject, s_body):
287
  if not (s_email and s_subject and s_body):
288
  return (
289
+ gr.update(), gr.update(), gr.update(), gr.update(),
290
+ _status_html("Fill in sender email, subject, and body.", False),
291
+ _pipeline_html("TRIAGE"), "", "",
 
292
  )
293
  try:
294
+ data = api_process_email(token, thread_id, s_email, s_subject, s_body)
295
+ label = data.get("triage_label", "")
296
  badge = _triage_badge_html(label)
297
 
298
  if data.get("status") == "needs_review":
 
301
  gr.update(visible=False), # process_group
302
  gr.update(visible=True), # review_group
303
  gr.update(visible=False), # send_group
304
+ gr.update(visible=False), # result_group
305
+ "",
306
  _pipeline_html("REVIEW", ["TRIAGE", "DRAFT"]),
307
  badge,
308
  _draft_card_html(draft.get("to"), draft.get("subject"), draft.get("body")),
 
309
  )
310
  else:
311
+ # No follow-up needed
312
+ if label == "NO_ACTION":
313
+ icon, title, detail = "βœ…", "No Action Needed", "The agent filed this email. No reply required."
314
+ else:
315
+ icon = "🚫"
316
+ title = "Quarantined / No Reply"
317
+ detail = f"The agent decided no reply is needed for this email."
318
+
319
+ card = _result_card_html(icon, title, detail, badge)
 
320
  return (
321
  gr.update(visible=False),
322
  gr.update(visible=False),
323
  gr.update(visible=False),
324
+ gr.update(visible=True, value=card),
325
  "",
326
+ _pipeline_html("TRIAGE", ["TRIAGE"]),
327
  badge,
328
  "",
 
329
  )
330
  except Exception as e:
331
  return (
332
+ gr.update(), gr.update(), gr.update(), gr.update(),
333
+ _status_html(f"Error: {e}", False),
334
+ _pipeline_html("TRIAGE"), "", "",
335
  )
336
 
337
  process_btn.click(
338
  do_process,
339
  inputs=[token_state, thread_state, sender_email, sender_subject, sender_body],
340
+ outputs=[process_group, review_group, send_group, result_group,
341
+ process_status, pipeline_disp, triage_badge_disp, draft_html],
 
 
342
  )
343
 
344
+ # ── Review ────────────────────────────────────────────────
345
+ changes_btn.click(lambda: gr.update(visible=True), outputs=[feedback_group])
 
 
 
346
 
 
347
  def do_review(token, thread_id, status, feedback):
348
  try:
349
  data = api_review_action(token, thread_id, status, feedback)
 
350
  if data.get("status") == "needs_review":
 
351
  draft = data.get("email_draft", {})
352
  return (
353
+ gr.update(visible=True), gr.update(visible=False),
 
354
  _draft_card_html(draft.get("to"), draft.get("subject"), draft.get("body")),
355
+ gr.update(visible=False), "",
356
+ _status_html("Agent redrafted β€” review the new version.", True),
 
357
  _pipeline_html("REVIEW", ["TRIAGE", "DRAFT"]),
358
  )
359
  elif data.get("draft_id"):
360
  return (
361
+ gr.update(visible=False), gr.update(visible=True),
362
+ gr.update(), gr.update(visible=False), "",
363
+ _status_html("Draft approved β€” now give a send command.", True),
 
 
 
364
  _pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW"]),
365
  )
366
  else:
367
  return (
368
+ gr.update(visible=False), gr.update(visible=True),
369
+ gr.update(), gr.update(visible=False), "", "",
370
+ _pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW"]),
371
  )
372
  except Exception as e:
373
  return (
374
  gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
375
+ _status_html(f"Review failed: {e}", False), gr.update(),
376
  )
377
 
378
+ _rev_out = [review_group, send_group, draft_html,
379
+ feedback_group, feedback_text, review_status, pipeline_disp]
 
 
 
 
 
 
 
 
 
380
 
381
+ approve_btn.click(lambda t, tid: do_review(t, tid, "approved", None),
382
+ inputs=[token_state, thread_state], outputs=_rev_out)
383
+ reject_btn.click(lambda t, tid: do_review(t, tid, "rejected", None),
384
+ inputs=[token_state, thread_state], outputs=_rev_out)
385
+ submit_feedback_btn.click(lambda t, tid, fb: do_review(t, tid, "rejected", fb),
386
+ inputs=[token_state, thread_state, feedback_text],
387
+ outputs=_rev_out)
388
 
389
+ # ── Send ──────────────────────────────────────────────────
390
  def do_send(token, thread_id, command_text):
391
  try:
392
+ data = api_send_email(token, thread_id, command_text)
393
+ msg_id = data.get("sent_message_id", "")
394
+ detail = f"Message ID: {msg_id}" if msg_id else "Email dispatched successfully."
395
+ card = _result_card_html("πŸ“¨", "Email Sent!", detail)
 
 
 
 
 
 
396
  return (
397
+ gr.update(visible=False),
398
+ gr.update(visible=True, value=card),
399
  "",
400
  _pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW", "SEND"]),
401
  )
402
  except Exception as e:
403
  return (
404
  gr.update(), gr.update(),
405
+ _status_html(f"Send failed: {e}", False), gr.update(),
406
  )
407
 
408
  send_btn.click(
409
  do_send,
410
  inputs=[token_state, thread_state, send_command],
411
+ outputs=[send_group, result_group, send_status, pipeline_disp],
412
  )
413
 
414
+ # ── Reset ─────────────────────────────────────────────────
415
  def start_new():
416
+ tid = str(uuid.uuid4())[:8]
417
  return (
418
+ tid, tid,
419
+ gr.update(visible=True), gr.update(visible=False),
420
+ gr.update(visible=False), gr.update(visible=False),
421
+ "", "", "", "", "",
422
+ gr.update(visible=False), "",
 
 
 
423
  _pipeline_html("TRIAGE"), "",
424
  )
425
 
426
  new_email_btn.click(
427
  start_new,
428
+ outputs=[thread_state, thread_display,
429
+ process_group, review_group, send_group, result_group,
430
+ sender_email, sender_subject, sender_body,
431
+ send_command, draft_html,
432
+ feedback_group, feedback_text,
433
+ pipeline_disp, triage_badge_disp],
 
 
434
  )
435
 
436
+ return demo