EmailAgentwithMemory / app /gradio_ui.py
Vinit006's picture
Update app/gradio_ui.py
98d4f58 verified
Raw
History Blame Contribute Delete
19.9 kB
"""
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'<span style="'
f'display:inline-block;padding:4px 14px;border-radius:20px;'
f'font-family:monospace;font-size:11px;font-weight:700;'
f'letter-spacing:0.06em;margin-right:8px;'
f'border:1px solid {border};color:{color};background:{bg};">'
f'{label}</span>'
)
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'<div style="padding:10px 0 6px;">{pills}</div>'
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'<div style="margin-top:6px;">'
f'<span style="display:inline-block;font-family:monospace;font-size:12px;'
f'font-weight:700;letter-spacing:0.05em;padding:5px 14px;border-radius:6px;'
f'background:{bg};color:{color};border:1px solid {border};">'
f'{label}</span></div>'
)
def _draft_card_html(to, subject, body) -> str:
to = to or "β€”"
subject = subject or "β€”"
body = (body or "β€”").replace("\n", "<br>")
return f"""
<div style="background:{C['surface']};border:1px solid {C['border']};
border-radius:12px;padding:20px;margin-top:8px;">
<div style="display:flex;gap:40px;margin-bottom:14px;flex-wrap:wrap;">
<div>
<div style="font-family:monospace;font-size:10px;letter-spacing:0.1em;
color:{C['dim']};text-transform:uppercase;margin-bottom:4px;">TO</div>
<div style="font-family:monospace;font-size:13px;color:{C['text']};">{to}</div>
</div>
<div>
<div style="font-family:monospace;font-size:10px;letter-spacing:0.1em;
color:{C['dim']};text-transform:uppercase;margin-bottom:4px;">SUBJECT</div>
<div style="font-size:13px;color:{C['text']};">{subject}</div>
</div>
</div>
<div style="height:1px;background:{C['border']};margin-bottom:14px;"></div>
<div style="font-size:13px;line-height:1.75;color:{C['muted']};">{body}</div>
</div>
"""
def _result_card_html(icon: str, title: str, detail: str, badge_html: str = "") -> str:
return f"""
<div style="background:{C['surface']};border:1px solid {C['border']};
border-radius:12px;padding:40px 32px;text-align:center;margin-top:8px;">
<div style="font-size:44px;margin-bottom:14px;">{icon}</div>
<div style="font-size:18px;font-weight:600;color:{C['text']};margin-bottom:8px;">{title}</div>
<div style="font-size:13px;color:{C['muted']};margin-bottom:12px;">{detail}</div>
{badge_html}
</div>
"""
def _status_html(msg: str, ok: bool) -> str:
color = C["success"] if ok else C["danger"]
icon = "βœ“" if ok else "βœ•"
return (
f'<div style="margin-top:6px;font-size:13px;color:{color};font-weight:500;">'
f'{icon} {msg}</div>'
)
# ─── 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'<div style="background:{C["accent_dim"]};border:1px solid {C["accent"]};'
f'border-radius:20px;padding:5px 14px;display:inline-block;'
f'font-family:monospace;font-size:12px;color:{C["accent"]};">'
f'Signed in as: {data["email"]}</div>'
)
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