import gradio as gr import uuid import logging import re import json import os import tempfile from agent_module import chat_agent_stream from translation_module import localize_init logger = logging.getLogger("app.ui") PREMIUM_CSS = """ body, .gradio-container { background-color: #0b0f19 !important; color: #e0e6ed !important; font-family: 'Inter', sans-serif !important; } .main-container { max-width: 900px !important; margin: 0 auto !important; min-height: 600px !important; max-height: 90vh !important; display: flex !important; flex-direction: column !important; padding: 10px !important; background: #0b0f19 !important; } .header-row { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: 10px !important; margin-bottom: 10px !important; padding: 10px !important; background: rgba(255, 255, 255, 0.05) !important; border-radius: 12px !important; } .chat-window { flex-grow: 1 !important; border-radius: 12px !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; background: rgba(0, 0, 0, 0.2) !important; overflow-y: auto !important; } .input-area { margin-top: 10px !important; background: rgba(255, 255, 255, 0.05) !important; padding: 10px !important; border-radius: 12px !important; flex-shrink: 0 !important; } footer { display: none !important; } @media (max-width: 768px) { .main-container { padding: 5px !important; height: auto !important; max-height: none !important; } .header-row { padding: 5px !important; flex-wrap: wrap !important; } } """ def save_and_clear(msg): return "", msg def export_chat(history): if not history: return None with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tf: json.dump(history, tf, indent=2) return tf.name def import_chat(file): if file is None: return gr.update(), [] try: with open(file.name, 'r') as f: data = json.load(f) if isinstance(data, list): return data, data except Exception as e: logger.error(f"Import error: {e}") return gr.update(), gr.update() def clear_messages(lang_name="English"): name_map = {"German": "de", "French": "fr", "Spanish": "es", "Italian": "it", "Portuguese": "pt", "Russian": "ru", "Japanese": "ja", "Chinese": "zh"} code = name_map.get(lang_name, "en") loc = localize_init(code) welcome = [{"role": "assistant", "content": loc["welcome"]}] return welcome, welcome def chat_wrapper(message, history, short_answers=False, lang="English"): if history is None: history = [] history.append({"role": "user", "content": message}) history.append({"role": "assistant", "content": ""}) # Intent detection is now handled internally by chat_agent_stream for part in chat_agent_stream(message, history[:-2], user_lang=lang, short_answers=short_answers): if part == "__TURN_END__": history.append({"role": "assistant", "content": ""}) yield history, history else: history[-1]["content"] = part yield history, history yield history, history def on_load(request: gr.Request): headers = dict(request.headers) lang_code = "en" try: if "accept-language" in headers: lang_code = headers.get("accept-language").split(",")[0].split("-")[0] except: pass loc = localize_init(lang_code) welcome = [{"role": "assistant", "content": loc["welcome"]}] return welcome, loc["lang"], gr.update(label=loc["label_short"]), gr.update(placeholder=loc["placeholder"]) def build_demo(enable_reload=True): with gr.Blocks(css=PREMIUM_CSS, title="Sage 6.5 - Oracle Intermediary") as demo: history_state = gr.State([]) lang_state = gr.State("English") saved_msg = gr.State("") # Default Static Localization for Immediate Render default_loc = localize_init("en") default_welcome = [{"role": "assistant", "content": default_loc["welcome"]}] with gr.Column(elem_classes="main-container"): with gr.Row(elem_classes="header-row"): gr.Markdown("## 🔮 Sage 6.5") with gr.Row(): import_btn = gr.UploadButton("📥 Import", file_types=[".json"], size="sm") export_btn = gr.DownloadButton("📤 Export", size="sm") # BEST PRACTICE: Init with value, don't wait for load chatbot = gr.Chatbot(value=default_welcome, elem_classes="chat-window", type="messages", bubble_full_width=False, show_label=False) with gr.Row(elem_classes="input-area"): msg_input = gr.Textbox(placeholder=default_loc["placeholder"], show_label=False, container=False, scale=4) submit_btn = gr.Button("➤", scale=1, variant="primary") short_answers = gr.Checkbox(label=default_loc["label_short"], value=False) # Wire Events msg_input.submit(save_and_clear, [msg_input], [msg_input, saved_msg], queue=False).then( chat_wrapper, [saved_msg, history_state, short_answers, lang_state], [chatbot, history_state] ) submit_btn.click(save_and_clear, [msg_input], [msg_input, saved_msg], queue=False).then( chat_wrapper, [saved_msg, history_state, short_answers, lang_state], [chatbot, history_state] ) export_btn.click(export_chat, [history_state], [export_btn]) import_btn.upload(import_chat, [import_btn], [chatbot, history_state]) chatbot.clear(clear_messages, [lang_state], [chatbot, history_state]) # State Sync must happen after init # We manually sync state with the default value so backend has it def sync_init(): return default_welcome, default_welcome # If reload is enabled (client-side), we still try to localize dynamically, but we have a safe fallback now. if enable_reload: demo.load(on_load, None, [chatbot, lang_state, short_answers, msg_input]) else: # If no reload, ensure history state matches visual init demo.load(sync_init, None, [history_state, chatbot]) # Sync state api_history = gr.Chatbot(visible=False, type="messages") api_chat_btn = gr.Button("API CHAT", visible=False) api_chat_btn.click(chat_wrapper, [msg_input, api_history, short_answers, lang_state], [chatbot, history_state], api_name="chat") return demo