"""
gradio_ui.py — inline-styles only, no CSS class dependencies
Works reliably on HF Spaces where Gradio scopes/blocks external CSS.
"""
import uuid
import requests
import gradio as gr
BASE_URL = "https://vinit006-emailagentwithmemory.hf.space"
# Colors as Python constants — used in inline styles throughout
C = {
"bg": "#0D1117",
"surface": "#161B22",
"surface2": "#1C2333",
"border": "#30363D",
"accent": "#58A6FF",
"accent_dim": "#1a2738",
"success": "#3FB950",
"success_dim": "#122a18",
"warning": "#D29922",
"warning_dim": "#2c2310",
"danger": "#F85149",
"danger_dim": "#2c1414",
"text": "#E6EDF3",
"muted": "#8B949E",
"dim": "#6E7681",
}
# ─── HTML builders (all inline styles) ──────────────────────────
def _pill(label: str, state: str) -> str:
"""state: 'idle' | 'active' | 'done'"""
if state == "active":
bg, border, color = C["accent_dim"], C["accent"], C["accent"]
elif state == "done":
bg, border, color = C["success_dim"], C["success"], C["success"]
else:
bg, border, color = C["surface2"], C["border"], C["dim"]
return (
f''
f'{label}'
)
def _pipeline_html(active_step: str, done_steps=None) -> str:
done_steps = done_steps or []
steps = ["TRIAGE", "DRAFT", "REVIEW", "SEND"]
pills = ""
for s in steps:
if s == active_step:
pills += _pill(s, "active")
elif s in done_steps:
pills += _pill(s, "done")
else:
pills += _pill(s, "idle")
return f'
{pills}
'
def _triage_badge_html(label: str) -> str:
if not label:
return ""
if label == "FOLLOW_UP_REQUIRED":
bg, border, color = C["warning_dim"], C["warning"], C["warning"]
elif label == "NO_ACTION":
bg, border, color = C["success_dim"], C["success"], C["success"]
else:
bg, border, color = C["accent_dim"], C["accent"], C["accent"]
return (
f''
f''
f'{label}
'
)
def _draft_card_html(to, subject, body) -> str:
to = to or "—"
subject = subject or "—"
body = (body or "—").replace("\n", "
")
return f"""
"""
def _result_card_html(icon: str, title: str, detail: str, badge_html: str = "") -> str:
return f"""
{icon}
{title}
{detail}
{badge_html}
"""
def _status_html(msg: str, ok: bool) -> str:
color = C["success"] if ok else C["danger"]
icon = "✓" if ok else "✕"
return (
f''
f'{icon} {msg}
'
)
# ─── API calls ──────────────────────────────────────────────────
def api_get_user_data(user_email):
res = requests.post(f"{BASE_URL}/get-user-data", params={"user_email": user_email})
res.raise_for_status()
return res.json()
def api_process_email(token, thread_id, sender_email, subject, body):
res = requests.post(
f"{BASE_URL}/process-email",
headers={"Authorization": f"Bearer {token}"},
json={"thread_id": thread_id, "sender_email_id": sender_email,
"sender_subject": subject, "sender_email_body": body},
)
res.raise_for_status()
return res.json()
def api_review_action(token, thread_id, status, feedback=None):
payload = {"thread_id": thread_id, "status": status}
if feedback:
payload["feedback"] = feedback
res = requests.post(
f"{BASE_URL}/review-action",
headers={"Authorization": f"Bearer {token}"},
json=payload,
)
res.raise_for_status()
return res.json()
def api_send_email(token, thread_id, command_text):
res = requests.post(
f"{BASE_URL}/send_email",
headers={"Authorization": f"Bearer {token}"},
json={"thread_id": thread_id, "human_message": command_text or "Send now."},
)
res.raise_for_status()
return res.json()
# ─── Build UI ────────────────────────────────────────────────────
def build_demo() -> gr.Blocks:
with gr.Blocks(title="Email Agent — AI Triage & Reply") as demo:
token_state = gr.State(None)
thread_state = gr.State(None)
gr.Markdown("# ⚡ Email Agent\n_AI-powered triage, drafting & sending — with a human in the loop._")
# ── Auth ─────────────────────────────────────────────────
with gr.Group(visible=True) as auth_group:
gr.Markdown("### Sign In")
with gr.Row():
email_input = gr.Textbox(label="Your Email", placeholder="you@example.com", scale=3)
login_btn = gr.Button("Sign In →", variant="primary", scale=1)
auth_status = gr.HTML("")
# ── Workspace ────────────────────────────────────────────
with gr.Group(visible=False) as workspace_group:
# Session bar
with gr.Row():
user_pill = gr.HTML("")
with gr.Row():
thread_display = gr.Textbox(label="Thread ID", interactive=True, scale=3)
new_thread_btn = gr.Button("↺ New Thread", scale=1)
pipeline_disp = gr.HTML(_pipeline_html("TRIAGE"))
triage_badge_disp = gr.HTML("")
gr.Markdown("---")
# Step 1
with gr.Group(visible=True) as process_group:
gr.Markdown("### 📥 Incoming Email")
with gr.Row():
sender_email = gr.Textbox(label="From (sender email)", placeholder="sender@company.com")
sender_subject = gr.Textbox(label="Subject", placeholder="Email subject line")
sender_body = gr.Textbox(label="Email Body", lines=8,
placeholder="Paste the full email body here…")
process_btn = gr.Button("⚡ Process Email", variant="primary")
process_status = gr.HTML("")
# Step 2
with gr.Group(visible=False) as review_group:
gr.Markdown("### ✍️ Review Draft")
draft_html = gr.HTML("")
with gr.Row():
approve_btn = gr.Button("✓ Approve & Finalize", variant="primary")
changes_btn = gr.Button("↺ Request Changes")
reject_btn = gr.Button("✕ Reject")
with gr.Group(visible=False) as feedback_group:
feedback_text = gr.Textbox(label="Feedback for agent", lines=4,
placeholder="Tell the agent what to change…")
submit_feedback_btn = gr.Button("Submit Feedback & Redraft")
review_status = gr.HTML("")
# Step 3
with gr.Group(visible=False) as send_group:
gr.Markdown(
"### 📨 Send Command\n"
"Draft saved in Gmail. Give the agent a send command.\n\n"
"> ℹ️ The agent can only **draft** and **send** — "
"it cannot edit content here. This is a timing command only."
)
send_command = gr.Textbox(
label="Send Command",
placeholder='"send now" / "send later today" / "hold off until tomorrow"',
)
send_btn = gr.Button("📨 Execute Send Command", variant="primary")
send_status = gr.HTML("")
# Step 4
with gr.Group(visible=False) as result_group:
result_html = gr.HTML("")
new_email_btn = gr.Button("Process Another Email", variant="primary")
# ── on_load: fresh thread ID per browser session ─────────
def on_load():
tid = str(uuid.uuid4())[:8]
return tid, tid
demo.load(on_load, outputs=[thread_state, thread_display])
# ── Login ─────────────────────────────────────────────────
def do_login(email):
if not email or "@" not in email:
return (gr.update(), gr.update(), None,
_status_html("Enter a valid email address.", False), "")
try:
data = api_get_user_data(email)
token = data["token"]
pill = (
f''
f'Signed in as: {data["email"]}
'
)
return (gr.update(visible=False), gr.update(visible=True),
token, "", pill)
except Exception as e:
return (gr.update(), gr.update(), None,
_status_html(f"Sign-in failed: {e}", False), "")
login_btn.click(
do_login, inputs=[email_input],
outputs=[auth_group, workspace_group, token_state, auth_status, user_pill],
)
# ── Thread ────────────────────────────────────────────────
def new_thread():
tid = str(uuid.uuid4())[:8]
return tid, tid
new_thread_btn.click(new_thread, outputs=[thread_state, thread_display])
thread_display.change(lambda v: v, inputs=[thread_display], outputs=[thread_state])
# ── Process Email ─────────────────────────────────────────
def do_process(token, thread_id, s_email, s_subject, s_body):
if not (s_email and s_subject and s_body):
return (
gr.update(), gr.update(), gr.update(), gr.update(),
_status_html("Fill in sender email, subject, and body.", False),
_pipeline_html("TRIAGE"), "", "",
)
try:
data = api_process_email(token, thread_id, s_email, s_subject, s_body)
label = data.get("triage_label", "")
badge = _triage_badge_html(label)
if data.get("status") == "needs_review":
draft = data.get("email_draft", {})
return (
gr.update(visible=False), # process_group
gr.update(visible=True), # review_group
gr.update(visible=False), # send_group
gr.update(visible=False), # result_group
"",
_pipeline_html("REVIEW", ["TRIAGE", "DRAFT"]),
badge,
_draft_card_html(draft.get("to"), draft.get("subject"), draft.get("body")),
)
else:
# No follow-up needed
if label == "NO_ACTION":
icon, title, detail = "✅", "No Action Needed", "The agent filed this email. No reply required."
else:
icon = "🚫"
title = "Quarantined / No Reply"
detail = f"The agent decided no reply is needed for this email."
card = _result_card_html(icon, title, detail, badge)
return (
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=True, value=card),
"",
_pipeline_html("TRIAGE", ["TRIAGE"]),
badge,
"",
)
except Exception as e:
return (
gr.update(), gr.update(), gr.update(), gr.update(),
_status_html(f"Error: {e}", False),
_pipeline_html("TRIAGE"), "", "",
)
process_btn.click(
do_process,
inputs=[token_state, thread_state, sender_email, sender_subject, sender_body],
outputs=[process_group, review_group, send_group, result_group,
process_status, pipeline_disp, triage_badge_disp, draft_html],
)
# ── Review ────────────────────────────────────────────────
changes_btn.click(lambda: gr.update(visible=True), outputs=[feedback_group])
def do_review(token, thread_id, status, feedback):
try:
data = api_review_action(token, thread_id, status, feedback)
if data.get("status") == "needs_review":
draft = data.get("email_draft", {})
return (
gr.update(visible=True), gr.update(visible=False),
_draft_card_html(draft.get("to"), draft.get("subject"), draft.get("body")),
gr.update(visible=False), "",
_status_html("Agent redrafted — review the new version.", True),
_pipeline_html("REVIEW", ["TRIAGE", "DRAFT"]),
)
elif data.get("draft_id"):
return (
gr.update(visible=False), gr.update(visible=True),
gr.update(), gr.update(visible=False), "",
_status_html("Draft approved — now give a send command.", True),
_pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW"]),
)
else:
return (
gr.update(visible=False), gr.update(visible=True),
gr.update(), gr.update(visible=False), "", "",
_pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW"]),
)
except Exception as e:
return (
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
_status_html(f"Review failed: {e}", False), gr.update(),
)
_rev_out = [review_group, send_group, draft_html,
feedback_group, feedback_text, review_status, pipeline_disp]
approve_btn.click(lambda t, tid: do_review(t, tid, "approved", None),
inputs=[token_state, thread_state], outputs=_rev_out)
reject_btn.click(lambda t, tid: do_review(t, tid, "rejected", None),
inputs=[token_state, thread_state], outputs=_rev_out)
submit_feedback_btn.click(lambda t, tid, fb: do_review(t, tid, "rejected", fb),
inputs=[token_state, thread_state, feedback_text],
outputs=_rev_out)
# ── Send ──────────────────────────────────────────────────
def do_send(token, thread_id, command_text):
try:
data = api_send_email(token, thread_id, command_text)
msg_id = data.get("sent_message_id", "")
detail = f"Message ID: {msg_id}" if msg_id else "Email dispatched successfully."
card = _result_card_html("📨", "Email Sent!", detail)
return (
gr.update(visible=False),
gr.update(visible=True, value=card),
"",
_pipeline_html("SEND", ["TRIAGE", "DRAFT", "REVIEW", "SEND"]),
)
except Exception as e:
return (
gr.update(), gr.update(),
_status_html(f"Send failed: {e}", False), gr.update(),
)
send_btn.click(
do_send,
inputs=[token_state, thread_state, send_command],
outputs=[send_group, result_group, send_status, pipeline_disp],
)
# ── Reset ─────────────────────────────────────────────────
def start_new():
tid = str(uuid.uuid4())[:8]
return (
tid, tid,
gr.update(visible=True), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
"", "", "", "", "",
gr.update(visible=False), "",
_pipeline_html("TRIAGE"), "",
)
new_email_btn.click(
start_new,
outputs=[thread_state, thread_display,
process_group, review_group, send_group, result_group,
sender_email, sender_subject, sender_body,
send_command, draft_html,
feedback_group, feedback_text,
pipeline_disp, triage_badge_disp],
)
return demo