Spaces:
Running
Running
| import base64, os, uuid, gradio as gr | |
| from pathlib import Path | |
| from typing import Dict, List, Tuple | |
| from langfuse import Langfuse, observe, get_client | |
| from langfuse.openai import openai | |
| langfuse = Langfuse(secret_key=os.getenv('LANGFUSE_SECRET_KEY'), public_key=os.getenv('LANGFUSE_PUBLIC_KEY'), host=os.getenv('LANGFUSE_HOST')) | |
| langfuse = get_client() | |
| BASE_URL = os.getenv('CHAT_BASE_URL') | |
| MODEL_NAME = os.getenv('MODEL_NAME') | |
| API_KEY = os.getenv('API_KEY') | |
| SYSTEM_PROMPT = (os.getenv('SYSTEM_PROMPT')) | |
| LOGO_PATH = Path(__file__).parent / "logo.png" | |
| def _load_logo_data_uri() -> str: | |
| try: | |
| encoded = base64.b64encode(LOGO_PATH.read_bytes()).decode("ascii") | |
| return f"data:image/png;base64,{encoded}" | |
| except FileNotFoundError: return "" | |
| LOGO_DATA_URI = _load_logo_data_uri() | |
| HERO_LOGO_HTML = ( | |
| f'<div class="hero-logo"><img src="{LOGO_DATA_URI}" alt="OpenJAI logo" /></div>' | |
| if LOGO_DATA_URI | |
| else "" | |
| ) | |
| QUICK_PROMPTS: List[str] = ["Who is JTS?", "What is the benefit of small LLM?", "คุณเป็นใคร", "AI ทำงานยังไง",] | |
| HERO_SECTION = f""" | |
| <div class="hero-banner"> | |
| <div class="hero-content"> | |
| {HERO_LOGO_HTML} | |
| <div class="hero-copy"> | |
| <span class="badge">Introducing OpenJAI-v1.0</span> | |
| <p>Partner with OpenJAI to deliver exceptional Thai language experiences, manage long-form context, and power reliable RAG-ready workflows. Purpose-built by the JTS research team.</p> | |
| <div class="hero-highlights"> | |
| <span>⚡ Optimized for Thai and English</span> | |
| <span>🧠 Resilient in long-context RAG</span> | |
| <span>🧩 Compact & efficient</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| INSIGHTS_HTML = """ | |
| <div class="surface-card"> | |
| <h3>Why OpenJAI?</h3> | |
| <ul class="feature-grid"> | |
| <li> | |
| <span class="feature-icon">🧡</span> | |
| <div> | |
| <h4>Better Thai and English text comprehension</h4> | |
| </div> | |
| </li> | |
| <li> | |
| <span class="feature-icon">🪄</span> | |
| <div> | |
| <h4>RAG-focused</h4> | |
| <p>Stronger understanding of long-context inputs.</p> | |
| </div> | |
| </li> | |
| <li> | |
| <span class="feature-icon">🛡️</span> | |
| <div> | |
| <h4>Robust & lightweight</h4> | |
| <p>Compact 14B model designed for efficiency with a strong performance.</p> | |
| </div> | |
| </li> | |
| </ul> | |
| </div> | |
| <div class="surface-card secondary"> | |
| <h3>Learn more at</h3> | |
| <ul class="moments-list"> | |
| <li> | |
| <a href="https://arxiv.org/abs/2510.06847" target="_blank" rel="noopener noreferrer" style="color: white;"> | |
| <strong>Technical Report</strong> | |
| </a> | |
| </li> | |
| </ul> | |
| </div> | |
| """ | |
| FOOTER_HTML = """ | |
| <div class="footer-note"> | |
| Crafted for Thai innovators by <strong>JTS</strong>. Share your ideas with us at <a href="mailto:jts.ai.team@gmail.com">jts.ai.team@gmail.com</a>. | |
| </div> | |
| """ | |
| CUSTOM_CSS = """:root, | |
| [data-theme="light"], | |
| [data-theme="dark"] { | |
| --bg-primary: #1f130b; | |
| --bg-gradient-1: rgba(255, 173, 96, 0.22); | |
| --bg-gradient-2: rgba(255, 113, 91, 0.2); | |
| --text-primary: #fff3e6; | |
| --text-secondary: rgba(255, 244, 235, 0.88); | |
| --text-tertiary: rgba(255, 234, 214, 0.82); | |
| --hero-bg-1: rgba(255, 159, 67, 0.28); | |
| --hero-bg-2: rgba(255, 98, 0, 0.32); | |
| --hero-border: rgba(255, 240, 225, 0.24); | |
| --hero-shadow: rgba(110, 48, 0, 0.45); | |
| --card-bg: rgba(54, 24, 6, 0.78); | |
| --card-bg-secondary: rgba(64, 26, 8, 0.8); | |
| --card-border: rgba(255, 202, 149, 0.22); | |
| --card-shadow: rgba(48, 20, 4, 0.5); | |
| --chat-bg: rgba(43, 21, 7, 0.86); | |
| --chat-border: rgba(255, 204, 152, 0.2); | |
| --message-bot-bg: rgba(255, 199, 150, 0.08); | |
| --message-user-bg: rgba(255, 142, 78, 0.22); | |
| --input-bg: rgba(36, 18, 9, 0.72); | |
| --input-border: rgba(255, 198, 150, 0.18); | |
| --input-focus: rgba(255, 183, 92, 0.65); | |
| --badge-bg: rgba(255, 255, 255, 0.16); | |
| --highlight-bg: rgba(92, 45, 7, 0.6); | |
| --highlight-border: rgba(255, 205, 169, 0.32); | |
| } | |
| :root, | |
| [data-theme="light"], | |
| [data-theme="dark"] { | |
| --block-background-fill: transparent; | |
| --body-background-fill: #1f130b; | |
| --background-fill-primary: #1f130b; | |
| --body-text-color: #fff3e6; | |
| body { | |
| margin: 0; | |
| background: radial-gradient(circle at 20% 20%, var(--bg-gradient-1), transparent 45%), | |
| radial-gradient(circle at 85% 10%, var(--bg-gradient-2), transparent 40%), | |
| var(--bg-primary); | |
| color: var(--text-primary); | |
| font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| } | |
| .gradio-container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 40px 28px 80px !important; | |
| background: transparent; | |
| } | |
| .hero-banner { | |
| position: relative; | |
| padding: 52px 48px 56px; | |
| border-radius: 28px; | |
| background: linear-gradient(135deg, var(--hero-bg-1), var(--hero-bg-2)); | |
| backdrop-filter: blur(18px); | |
| border: 1px solid var(--hero-border); | |
| overflow: hidden; | |
| box-shadow: 0 36px 120px var(--hero-shadow); | |
| } | |
| .hero-banner::after { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| background: radial-gradient(circle at 18% 22%, rgba(255, 255, 255, 0.18), transparent 55%); | |
| pointer-events: none; | |
| } | |
| .hero-content { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| gap: 32px; | |
| } | |
| .hero-logo { | |
| flex: 0 0 auto; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 18px; | |
| border-radius: 24px; | |
| background: var(--badge-bg); | |
| border: 1px solid var(--hero-border); | |
| box-shadow: 0 18px 50px var(--card-shadow); | |
| } | |
| .hero-logo img { | |
| width: 120px; | |
| height: auto; | |
| display: block; | |
| } | |
| .hero-copy { | |
| flex: 1 1 auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| min-width: 0; | |
| } | |
| .hero-banner h1 { | |
| font-size: 48px; | |
| line-height: 1.05; | |
| margin: 18px 0 18px; | |
| color: var(--text-primary); | |
| } | |
| .hero-banner p { | |
| max-width: 640px; | |
| font-size: 19px; | |
| color: var(--text-secondary); | |
| margin-bottom: 26px; | |
| } | |
| .badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: var(--badge-bg); | |
| border-radius: 999px; | |
| padding: 6px 16px; | |
| font-size: 14px; | |
| letter-spacing: 0.4px; | |
| text-transform: uppercase; | |
| color: var(--text-primary); | |
| font-weight: 600; | |
| } | |
| .hero-highlights { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| } | |
| .hero-highlights span { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 18px; | |
| border-radius: 999px; | |
| background: var(--highlight-bg); | |
| border: 1px solid var(--highlight-border); | |
| font-size: 15px; | |
| color: var(--text-primary); | |
| font-weight: 500; | |
| } | |
| .content-row { | |
| margin-top: 42px; | |
| gap: 24px; | |
| } | |
| .chat-column { | |
| gap: 16px; | |
| } | |
| .chat-panel { | |
| border-radius: 24px !important; | |
| background: var(--chat-bg) !important; | |
| border: 1px solid var(--chat-border) !important; | |
| box-shadow: 0 28px 80px var(--card-shadow); | |
| } | |
| .chat-panel .message.bot { | |
| background: var(--message-bot-bg) !important; | |
| border-radius: 18px; | |
| color: var(--text-primary) !important; | |
| } | |
| .chat-panel .message.user { | |
| background: var(--message-user-bg) !important; | |
| border-radius: 18px; | |
| color: var(--text-primary) !important; | |
| } | |
| .compose-panel { | |
| padding: 10px; | |
| border-radius: 20px; | |
| # background: var(--chat-bg) !important; | |
| border: 1px solid var(--card-border); | |
| # box-shadow: inset 0 0 0 1px var(--input-border); | |
| gap: 0px; | |
| } | |
| .compose-panel textarea { | |
| background: var(--chat-bg) !important; | |
| border: 1px solid var(--input-border); | |
| border-radius: 16px; | |
| font-size: 16px; | |
| min-height: 94px !important; | |
| } | |
| .compose-panel textarea:focus { | |
| border-color: black; | |
| box-shadow: 0 0 0 3px rgba(255, 162, 53, 0.22); | |
| } | |
| .actions-row { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 14px; | |
| } | |
| .primary-btn { | |
| background: linear-gradient(135deg, #ff9933, #ff6a2b) !important; | |
| border: none !important; | |
| color: #fff !important; | |
| font-weight: 600 !important; | |
| padding: 12px 26px !important; | |
| border-radius: 14px !important; | |
| } | |
| .primary-btn:hover { | |
| filter: brightness(1.08); | |
| } | |
| .ghost-btn { | |
| background: linear-gradient(135deg, #ff9933, #ff6a2b) !important; | |
| border: none !important; | |
| color: var(--text-primary) !important; | |
| font-weight: 500 !important; | |
| padding: 12px 26px !important; | |
| border-radius: 14px !important; | |
| } | |
| .quick-prompts { | |
| margin-top: 10px; | |
| padding: 24px; | |
| border: 1px solid var(--input-border); | |
| border-radius: 16px; | |
| # background-color: transparent !important; | |
| } | |
| .quick-prompts .label { | |
| font-size: 17px; | |
| color: #CC7000; | |
| # color: var(--text-tertiary); | |
| # background-color: transparent !important; | |
| margin-bottom: 14px; | |
| padding: 0; | |
| font-weight: 600; | |
| } | |
| .quick-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 12px; | |
| } | |
| .quick-chip { | |
| justify-content: flex-start !important; | |
| border-radius: 14px !important; | |
| background: var(--card-bg) !important; | |
| border: 1px solid var(--card-border) !important; | |
| color: var(--text-primary) !important; | |
| font-weight: 500 !important; | |
| min-height: 54px !important; | |
| } | |
| .quick-chip:hover { | |
| border-color: var(--input-focus) !important; | |
| transform: translateY(-1px); | |
| } | |
| .info-column { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 18px; | |
| } | |
| .surface-card { | |
| padding: 28px; | |
| border-radius: 24px; | |
| background: var(--card-bg); | |
| border: 1px solid var(--card-border); | |
| box-shadow: 0 24px 70px var(--card-shadow); | |
| margin-bottom: 24px; | |
| } | |
| .surface-card.secondary { | |
| background: var(--card-bg-secondary); | |
| } | |
| .surface-card h3 { | |
| margin-top: 0; | |
| font-size: 22px; | |
| color: var(--text-primary); | |
| } | |
| .feature-grid { | |
| list-style: none; | |
| padding: 0; | |
| margin: 18px 0 0; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 18px; | |
| } | |
| .feature-grid li { | |
| display: flex; | |
| gap: 18px; | |
| align-items: flex-start; | |
| } | |
| .feature-icon { | |
| font-size: 26px; | |
| line-height: 1; | |
| } | |
| .feature-grid h4 { | |
| margin: 0 0 4px; | |
| font-size: 18px; | |
| color: var(--text-primary); | |
| } | |
| .feature-grid p { | |
| margin: 0; | |
| color: var(--text-secondary); | |
| line-height: 1.55; | |
| } | |
| .moments-list { | |
| list-style: none; | |
| padding: 0; | |
| margin: 16px 0 0; | |
| display: grid; | |
| gap: 12px; | |
| } | |
| .moments-list li { | |
| color: var(--text-secondary); | |
| line-height: 1.55; | |
| } | |
| .footer-note { | |
| margin-top: 48px; | |
| text-align: center; | |
| color: var(--text-tertiary); | |
| font-size: 15px; | |
| } | |
| .footer-note a { | |
| color: #ff9933; | |
| font-weight: 500; | |
| text-decoration: none; | |
| } | |
| @media (max-width: 992px) { | |
| .hero-banner { | |
| padding: 36px 28px; | |
| } | |
| .hero-banner h1 { | |
| font-size: 38px; | |
| } | |
| .hero-content { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 24px; | |
| } | |
| .hero-logo { | |
| padding: 16px; | |
| } | |
| .content-row { | |
| flex-direction: column; | |
| } | |
| } | |
| @media (max-width: 640px) { | |
| .hero-banner { | |
| padding: 28px 24px; | |
| } | |
| .hero-content { | |
| gap: 18px; | |
| } | |
| .hero-highlights { | |
| flex-direction: column; | |
| } | |
| }""" | |
| try: client = openai.AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY) | |
| except Exception as exc: | |
| print(f"Error initializing OpenAI client: {exc}") | |
| client = None | |
| async def get_api_stream(messages: List[Dict[str, str]], user_id: str): | |
| """ | |
| This inner function is observed by Langfuse. | |
| It performs the API call and returns the raw stream object. | |
| """ | |
| if langfuse: langfuse.update_current_trace(user_id=user_id) | |
| stream = await client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=messages, | |
| extra_body={"chat_template_kwargs": {"enable_thinking": False}}, | |
| stream=True, | |
| max_tokens=4096, | |
| ) | |
| return stream | |
| async def stream_chat(history: List[Tuple[str, str]], user_message: str, user_id: str): | |
| """ | |
| This outer function is the generator that Gradio interacts with. | |
| It calls the observed inner function and yields updates to the UI. | |
| """ | |
| history = history or [] | |
| if not user_message or not user_message.strip(): | |
| yield history, history | |
| return | |
| working_history = history + [(user_message, "")] | |
| yield working_history, working_history | |
| if not client: | |
| working_history[-1] = (user_message, "❌ Error: AI client not configured.") | |
| yield working_history, working_history | |
| return | |
| messages: List[Dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}] | |
| for previous_user, previous_bot in history: | |
| messages.append({"role": "user", "content": previous_user}) | |
| messages.append({"role": "assistant", "content": previous_bot}) | |
| messages.append({"role": "user", "content": user_message}) | |
| assistant_text = "" | |
| try: | |
| stream = await get_api_stream(messages, user_id) | |
| async for chunk in stream: | |
| delta = chunk.choices[0].delta.content | |
| if delta: | |
| assistant_text += delta | |
| working_history[-1] = (user_message, assistant_text) | |
| yield working_history, working_history | |
| except Exception as error: | |
| working_history[-1] = (user_message, f"⚠️ Error: {error}") | |
| yield working_history, working_history | |
| def reset_conversation(): return [], [], "" | |
| with gr.Blocks( | |
| theme=gr.themes.Soft(primary_hue="orange"), | |
| title="OpenJAI-v1.0 · Modern AI Companion", | |
| css=CUSTOM_CSS, | |
| ) as demo: | |
| gr.HTML(HERO_SECTION) | |
| with gr.Row(elem_classes="content-row"): | |
| with gr.Column(scale=7, elem_classes="chat-column"): | |
| chatbot = gr.Chatbot( | |
| label="Conversation history", | |
| bubble_full_width=False, | |
| height=520, | |
| show_copy_button=True, | |
| elem_classes="chat-panel", | |
| ) | |
| with gr.Group(elem_classes="compose-panel"): | |
| prompt_input = gr.Textbox( | |
| show_label=False, | |
| placeholder="Type your message here...", | |
| lines=4, | |
| autofocus=True, | |
| ) | |
| with gr.Row(elem_classes="actions-row"): | |
| submit_button = gr.Button("Send", elem_classes="primary-btn") | |
| clear_button = gr.Button( | |
| "Reset Chat", elem_classes="ghost-btn", variant="secondary" | |
| ) | |
| with gr.Column(scale=5, min_width=320, elem_classes="info-column"): | |
| gr.HTML(INSIGHTS_HTML) | |
| with gr.Group(elem_classes="quick-prompts"): | |
| gr.HTML("<div class='label'>Suggest question:</div>") | |
| with gr.Row(elem_classes="quick-grid"): | |
| quick_buttons = [] | |
| for prompt in QUICK_PROMPTS: | |
| button = gr.Button(prompt, elem_classes="quick-chip", variant="secondary") | |
| quick_buttons.append((button, prompt)) | |
| gr.HTML(FOOTER_HTML) | |
| def create_session_uuid(): return str(uuid.uuid4()) | |
| history_state = gr.State([]) | |
| user_id_state = gr.State() | |
| demo.load(fn=create_session_uuid, inputs=None, outputs=user_id_state) | |
| submit_event = submit_button.click( | |
| fn=stream_chat, | |
| inputs=[history_state, prompt_input, user_id_state], | |
| outputs=[chatbot, history_state], | |
| ) | |
| submit_event.then(fn=lambda: "", inputs=None, outputs=prompt_input, queue=False) | |
| submit_input_event = prompt_input.submit( | |
| fn=stream_chat, | |
| inputs=[history_state, prompt_input, user_id_state], | |
| outputs=[chatbot, history_state], | |
| ) | |
| submit_input_event.then(fn=lambda: "", inputs=None, outputs=prompt_input, queue=False) | |
| clear_button.click( | |
| fn=reset_conversation, | |
| inputs=None, | |
| outputs=[chatbot, history_state, prompt_input], | |
| queue=False, | |
| ) | |
| for button, prompt in quick_buttons: | |
| button.click(fn=lambda p=prompt: p, inputs=None, outputs=prompt_input, queue=False) | |
| if __name__ == "__main__": demo.queue().launch() |