import os import io import time import base64 import gradio as gr import pandas as pd from PIL import Image from gradio_client import Client # ------------------------- Constants ------------------------- TABLEAU_URL = "https://public.tableau.com/views/InsuranceDashboard_17677520784850/ExecutiveOverviewDashbiard?:showVizHome=no&:embed=y&:toolbar=no" BACKEND_SPACE_ID = (os.getenv("BACKEND_SPACE_ID") or "").strip() HF_TOKEN = (os.getenv("HF_TOKEN") or "").strip() QUICK_QUESTIONS = [ "Loss ratio by product_type", "Loss ratio by region", "Claim frequency by product_type", "Average claim severity", "Top 10 agents by premium", ] # ---------------------Initialize once--------------------------------- client = Client(BACKEND_SPACE_ID, token=HF_TOKEN) # ------------------------- Backend helpers ------------------------- def decode_chart(chart_b64: str): """Decode a base64 PNG/JPEG string returned by the backend into a PIL image.""" if not chart_b64: return None raw = chart_b64 if "," in raw and raw.strip().startswith("data:image"): raw = raw.split(",", 1)[1] try: img_bytes = base64.b64decode(raw) return Image.open(io.BytesIO(img_bytes)) except Exception: return None def call_backend(query: str) -> tuple[pd.DataFrame, object, str]: """ Expected backend response format: { "data": [ { ...row... }, ... ], "chart": "", "message": "optional summary" } """ response = client.predict( query, api_name="/query" ) if not isinstance(response, dict): raise ValueError("Backend returned an unexpected response format.") df_res = pd.DataFrame(response.get("data", [])) chart_img = decode_chart(response.get("chart")) message = response.get("message", "Results generated.") return df_res, chart_img, message # ------------------------- UI + Streaming ------------------------- def _js_autoscroll(): return """ """ def _set_interactive(flag: bool): return gr.update(interactive=flag) def _thinking_steps(query: str): base = f"Query: {query}\nThinking" dots = ["", ".", "..", "..."] for i in range(4): yield f"{base}{dots[i]}" time.sleep(0.10) steps = [ "• Calling backend", "• Fetching results", "• Rendering results", ] acc = f"{base}...\n" for s in steps: acc = acc + s + "\n" yield acc.rstrip() time.sleep(0.15) def _to_messages(history_pairs: list[list[str]]): msgs = [] for pair in history_pairs or []: if not pair: continue user = pair[0] if len(pair) > 0 else "" assistant = pair[1] if len(pair) > 1 else "" if user: msgs.append({"role": "user", "content": str(user)}) if assistant: msgs.append({"role": "assistant", "content": str(assistant)}) return msgs def run_query(user_text: str, history_pairs: list): history_pairs = history_pairs or [] q = (user_text or "").strip() if not q: yield ( gr.update(value=""), history_pairs, gr.update(value=_to_messages(history_pairs)), gr.update(value=pd.DataFrame(), visible=False), gr.update(value=None, visible=False), gr.update(selected=0), _set_interactive(False), *[_set_interactive(True) for _ in range(len(QUICK_QUESTIONS))], _js_autoscroll(), ) return disabled_send = _set_interactive(False) disabled_chips = [_set_interactive(False) for _ in range(len(QUICK_QUESTIONS))] for frame in _thinking_steps(q): yield ( gr.update(value=frame), history_pairs, gr.update(value=_to_messages(history_pairs)), gr.update(value=pd.DataFrame(), visible=False), gr.update(value=None, visible=False), gr.update(), disabled_send, *disabled_chips, _js_autoscroll(), ) try: df_res, chart_img, assistant_msg = call_backend(q) except Exception as e: df_res = pd.DataFrame({"error": [str(e)]}) chart_img = None assistant_msg = f"Backend request failed: {e}" history_pairs = history_pairs + [[q, assistant_msg]] if (df_res is None or df_res.empty) and assistant_msg: df_res = pd.DataFrame({"message": [assistant_msg]}) show_df = df_res is not None and not df_res.empty show_fig = chart_img is not None tab_selected = 1 if show_fig else 0 enabled_chips = [_set_interactive(True) for _ in range(len(QUICK_QUESTIONS))] send_after = _set_interactive(False) yield ( gr.update(value=""), history_pairs, gr.update(value=_to_messages(history_pairs)), gr.update(value=(df_res if df_res is not None else pd.DataFrame()), visible=show_df), gr.update(value=chart_img, visible=show_fig), gr.update(selected=tab_selected), send_after, *enabled_chips, _js_autoscroll(), ) def chip_run(chip_text: str, history_pairs: list): yield from run_query(chip_text, history_pairs) def clear_all(): empty_pairs = [] return ( gr.update(value=""), empty_pairs, gr.update(value=_to_messages(empty_pairs)), gr.update(value=pd.DataFrame(), visible=False), gr.update(value=None, visible=False), gr.update(selected=0), gr.update(interactive=False), *[gr.update(interactive=True) for _ in range(len(QUICK_QUESTIONS))], _js_autoscroll(), ) def send_enabled(text: str): ok = bool((text or "").strip()) return gr.update(interactive=ok) # ------------------------- CSS ------------------------- CSS = """ :root{ --bg: #f6f7fb; --card: #ffffff; --muted: #64748b; --border: rgba(15, 23, 42, 0.10); --shadow: 0 10px 30px rgba(15, 23, 42, 0.08); --radius: 16px; } body{ background: var(--bg) !important; } #app_wrap{ height: 100vh; max-width: 1600px; margin: 0 auto; padding: 14px; box-sizing: border-box; } .split_row{ height: calc(100vh - 28px); gap: 14px; } #left_tableau{ height: calc(100vh - 28px); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--card); box-shadow: var(--shadow); } #left_tableau iframe{ width: 100%; height: calc(100vh - 28px); border: 0; } #right_chat{ height: calc(100vh - 28px); border: 1px solid var(--border); border-radius: var(--radius); background: var(--card); box-shadow: var(--shadow); display: flex; flex-direction: column; overflow: visible; } .copilot_header{ padding: 12px 14px; border-bottom: 1px solid rgba(15, 23, 42, 0.08); background: linear-gradient(180deg, #ffffff, #fbfbfe); font-weight: 700; } /* ONLY scroll container on right */ #chat_thread{ flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 14px 14px 120px 14px; background: var(--bg); box-sizing: border-box; } /* REMOVE chat message bubbles + toolbar */ #chat_thread .message, #chat_thread .message-wrap, #chat_thread .message-row, #chat_thread .bubble, #chat_thread .toolbar, #chat_thread .icon-row { display: none !important; } /* Collapse leftover Chatbot spacing */ #chat_thread .chatbot, #chat_thread .chatbot > div { margin: 0 !important; padding: 0 !important; height: 0 !important; } /* Dock */ #dock{ position: sticky; bottom: 0; z-index: 50; padding: 12px 14px 12px 14px; background: linear-gradient(180deg, rgba(246,247,251,0.95), rgba(246,247,251,1)); border-top: 1px solid rgba(15, 23, 42, 0.10); } #composer_card{ display: flex; gap: 10px; align-items: flex-end; padding: 10px 10px 10px 12px; border-radius: 18px; background: #ffffff; border: 1px solid rgba(15, 23, 42, 0.12); box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); } #composer{ flex: 1 1 auto; } #composer .wrap{ padding: 0 !important; } #composer textarea{ width: 100% !important; border: none !important; outline: none !important; background: transparent !important; font-size: 14px !important; line-height: 1.35 !important; min-height: 56px !important; max-height: 160px !important; resize: none !important; padding: 6px 6px 6px 2px !important; } #composer_actions{ display: flex; gap: 8px; align-items: center; justify-content: flex-end; flex: 0 0 auto; } #send_btn button{ height: 38px !important; padding: 0 14px !important; border-radius: 999px !important; border: 1px solid #111827 !important; background: #111827 !important; color: #fff !important; font-weight: 800 !important; } #clear_btn button{ height: 38px !important; padding: 0 12px !important; border-radius: 999px !important; border: 1px solid rgba(15, 23, 42, 0.18) !important; background: #ffffff !important; color: #111827 !important; font-weight: 800 !important; } #clear_btn button:hover{ background: #f8fafc !important; } #quick_chips .chips-row > div, #quick_chips .chips-row > .gradio-column, #quick_chips .chips-row .gradio-column{ flex: 0 0 auto !important; width: auto !important; min-width: 0 !important; } #chips{ margin-top: 10px; } .chips-wrap{ display: grid; gap: 8px; } .chips-title{ font-size: 12px; font-weight: 700; color: var(--muted); letter-spacing: 0.02em; } .chips-row{ display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } .chip{ appearance: none; border: 1px solid rgba(15, 23, 42, 0.16); background: #ffffff; border-radius: 999px; padding: 7px 11px; font-size: 12px; font-weight: 700; cursor: pointer; box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06); } .chip:hover{ background: #f8fafc; } .card{ background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05); padding: 12px; } #result_chart{ min-height: 360px; width: 100%; } #output_tabs{ margin-top: 4px !important; } """ # ------------------------- App ------------------------- with gr.Blocks(title="Converse AI", css=CSS) as demo: history_state = gr.State([]) with gr.Column(elem_id="app_wrap"): with gr.Row(elem_classes=["split_row"]): with gr.Column(scale=2, min_width=650): gr.HTML( f"""
""" ) with gr.Column(scale=1, min_width=420): with gr.Group(elem_id="right_chat"): with gr.Column(elem_id="chat_thread"): chat_ui = gr.Chatbot( value=[], show_label=False, label=None, height=None, ) with gr.Tabs(elem_id="output_tabs") as output_tabs: with gr.Tab("Results", id=0): result_df = gr.Dataframe( value=pd.DataFrame(), show_label=False, interactive=False, visible=False, wrap=True, elem_classes=["card"], ) with gr.Tab("Chart", id=1): result_chart = gr.Image( value=None, show_label=False, visible=False, elem_id="result_chart", elem_classes=["card"], ) js = gr.HTML(value=_js_autoscroll()) with gr.Column(elem_id="dock"): with gr.Row(elem_id="composer_card", equal_height=False): composer = gr.Textbox( placeholder="Ask a question…", show_label=False, lines=2, max_lines=8, elem_id="composer", container=False, ) with gr.Row(elem_id="composer_actions", equal_height=False): send_btn = gr.Button("Send", elem_id="send_btn", interactive=False) clear_btn = gr.Button("Clear", elem_id="clear_btn", variant="secondary") with gr.Column(elem_id="quick_chips"): gr.HTML( """
Quick questions
""" ) with gr.Row(elem_classes=["chips-row"]): chip_buttons = [] for q in QUICK_QUESTIONS: chip_buttons.append(gr.Button(q, variant="secondary", elem_classes=["chip"])) composer.change(send_enabled, inputs=[composer], outputs=[send_btn]) send_btn.click( run_query, inputs=[composer, history_state], outputs=[ composer, history_state, chat_ui, result_df, result_chart, output_tabs, send_btn, *chip_buttons, js, ], ) composer.submit( run_query, inputs=[composer, history_state], outputs=[ composer, history_state, chat_ui, result_df, result_chart, output_tabs, send_btn, *chip_buttons, js, ], ) for btn, q in zip(chip_buttons, QUICK_QUESTIONS): btn.click( chip_run, inputs=[gr.State(q), history_state], outputs=[ composer, history_state, chat_ui, result_df, result_chart, output_tabs, send_btn, *chip_buttons, js, ], ) clear_btn.click( clear_all, outputs=[ composer, history_state, chat_ui, result_df, result_chart, output_tabs, send_btn, *chip_buttons, js, ], ) if __name__ == "__main__": demo.queue().launch(ssr_mode=False)