""" 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"""
TO
{to}
SUBJECT
{subject}
{body}
""" 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