Spaces:
Sleeping
Sleeping
| """ | |
| 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 |