Spaces:
Sleeping
Sleeping
| 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": "<base64-encoded image or data URL>", | |
| "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 """ | |
| <script> | |
| (function(){ | |
| const t = document.getElementById("chat_thread"); | |
| if (t) t.scrollTop = t.scrollHeight; | |
| })(); | |
| </script> | |
| """ | |
| 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""" | |
| <div id="left_tableau"> | |
| <iframe src="{TABLEAU_URL}" allowfullscreen></iframe> | |
| </div> | |
| """ | |
| ) | |
| 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( | |
| """ | |
| <div class="chips-wrap"> | |
| <div class="chips-title">Quick questions</div> | |
| </div> | |
| """ | |
| ) | |
| 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) | |