gemma-sage / ui_module.py
neuralworm's picture
fix: Robust Welcome Message (Sync Init)
524e7b6
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