"""NeuroBait UI — hybrid: a standard AI-chat experience (``gr.ChatInterface``, DeepSeek/Perplexity-ish) carrying NeuroBait's identity. gradio-only (no torch/spaces) so the UI can be built and smoke-tested without the model stack. Theme + CSS are applied at ``launch()`` because Gradio 6 moved ``theme``/``css``/``js`` off the Blocks/ChatInterface constructor — see the deprecation warning we hit in the Space logs. Dark-only, earthy, calm sage-teal accent (gentler / more sensory-safe for ADHD than vivid green). The palette is set straight on ``:root`` and mapped onto Gradio's core CSS vars unconditionally, so the app is dark with no dependence on JS or HF's ``?__theme``. UX notes for ADHD users: - Fewer decisions: mood is 4 choices, examples are single-click starting points. - No shame framing: copy is warm, safety scope is visible, no streaks/deadlines. - Consistent language: the Gradio footer is forced to English so the whole app feels like one voice, regardless of browser locale. """ from __future__ import annotations import base64 from pathlib import Path import gradio as gr MOODS = ["Calm", "Tired", "Anxious", "Focused"] THEME = gr.themes.Soft(primary_hue="teal", neutral_hue="slate") TITLE = "" MARK_PATH = Path(__file__).with_name("neurobait-mark.png") DESCRIPTION = ( "ADHD-friendly companion. " "A warm space and a gentle boost for your everyday. " "No red pen · no pressure · no bullet points paralysis. " "Not diagnostic" ) # Force the Gradio shell footer to English so it does not clash with the app's # English copy when the viewer's browser locale is Indonesian. I18N_FOOTER = { "use_via_api": "Use via API", "built_with_gradio": "Built with Gradio", "settings": "Settings", } I18N = gr.I18n( en=I18N_FOOTER, id=I18N_FOOTER, ) MARK_SRC = f"data:image/png;base64,{base64.b64encode(MARK_PATH.read_bytes()).decode('ascii')}" HEADER_HTML = f"""
NeuroBait
task initiation companion
""" FOOTER_HTML = """ """ PLACEHOLDER = "What's on your mind? No need to be tidy." EXAMPLES = [ ["There's something I've been meaning to get to, and it keeps slipping by."], ["My space feels a bit cluttered today."], ["My mind feels a little foggy right now."], ] CSS = """ /* Earthy, always-dark palette on :root (no .dark dependency), with a calm muted sage-teal accent instead of vivid green. */ :root { --forest: #6fb6a2; --sage: #5da894; --mint: #20362f; --cream: #1d2128; --linen: #15181d; --sand: #2e333b; --stone: #aeb6bf; --charcoal: #eef1f4; --radius: 10px; --brand-grad: linear-gradient(135deg, #5da894, #4f9582); } /* map Gradio core theme vars onto our dark palette so built-in components render dark even when Gradio's own .dark class is off */ body, .gradio-container, gradio-app, .dark { --body-background-fill: var(--linen); --background-fill-primary: var(--cream); --background-fill-secondary: var(--linen); --block-background-fill: var(--cream); --block-label-background-fill: var(--cream); --block-border-color: var(--sand); --border-color-primary: var(--sand); --body-text-color: var(--charcoal); --body-text-color-subdued: var(--stone); --input-background-fill: var(--cream); --input-border-color: var(--sand); --button-secondary-background-fill: var(--cream); --button-secondary-text-color: var(--charcoal); } body, .gradio-container { background: var(--linen) !important; color: var(--charcoal) !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji" !important; } /* keep the conversation in a comfortable centered column like a modern chat app */ .gradio-container { max-width: 960px !important; margin: 0 auto !important; } h1 { letter-spacing: -0.5px; font-weight: 800; } .nb-header { display: flex; align-items: center; justify-content: flex-start; gap: 12px; padding: 1rem 0 0.4rem; } .nb-mark { width: 42px; height: 42px; object-fit: contain; flex: 0 0 auto; } .nb-wordmark { font-size: 1.24rem; line-height: 1.05; font-weight: 780; letter-spacing: 0; color: var(--charcoal); } .nb-kicker { margin-top: 3px; color: var(--stone); font-size: 0.78rem; line-height: 1.2; } /* user bubble gets the brand accent, assistant stays flat/quiet */ .message.user, [data-testid="user"] { background: var(--brand-grad) !important; color: #fff !important; border: none !important; } .message.bot, [data-testid="bot"] { background: var(--cream) !important; border: 1px solid var(--sand) !important; color: var(--charcoal) !important; } /* rounded, calm input + primary button */ textarea, input[type="text"] { border-radius: var(--radius) !important; background: var(--cream) !important; border: 1px solid var(--sand) !important; color: var(--charcoal) !important; } button.primary, .submit-button, [variant="primary"] { background: var(--brand-grad) !important; border: none !important; color: #fff !important; } /* ADHD-friendly examples: render as calm chips instead of a data table */ .examples table, [data-testid="examples"] table, .examples-wrap table { display: block !important; width: 100% !important; } .examples table thead, [data-testid="examples"] table thead, .examples-wrap table thead { display: none !important; } .examples table tbody, [data-testid="examples"] table tbody, .examples-wrap table tbody { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; } .examples table tr, [data-testid="examples"] table tr, .examples-wrap table tr { display: inline-flex !important; } .examples table td, [data-testid="examples"] table td, .examples-wrap table td { background: var(--cream) !important; border: 1px solid var(--sand) !important; border-radius: 999px !important; padding: 8px 14px !important; color: var(--charcoal) !important; cursor: pointer !important; font-size: 0.9rem !important; line-height: 1.4 !important; transition: background 0.15s ease, border-color 0.15s ease !important; } .examples table td:hover, [data-testid="examples"] table td:hover, .examples-wrap table td:hover { background: var(--mint) !important; border-color: var(--forest) !important; } /* Make the empty-state placeholder feel centered and readable */ .chatbot .placeholder, [data-testid="chatbot"] .placeholder { text-align: center !important; display: flex !important; align-items: center !important; justify-content: center !important; height: 100% !important; opacity: 0.9 !important; } /* Safety scope footer at the bottom of the app */ .gradio-container::after { content: "Not a medical device · not a therapist · just a gentle nudge"; display: block; text-align: center; color: var(--stone); font-size: 0.75rem; padding: 1.5rem 0 1rem; margin-top: 1rem; border-top: 1px solid var(--sand); } .nb-footer-links { display: flex; align-items: center; justify-content: center; gap: 9px; color: var(--stone); font-size: 0.8rem; margin: 1.75rem 0 0.25rem; } .nb-footer-links a, .nb-footer-links span { color: var(--stone) !important; text-decoration: none !important; } .nb-footer-links a:hover { color: var(--charcoal) !important; } """ JS = """ () => { const replacements = [ ["Gunakan melalui API", "Use via API"], ["Dibuat dengan Gradio", "Built with Gradio"], ["Pengaturan", "Settings"], ]; const normalizeFooter = () => { const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); const textNodes = []; while (walker.nextNode()) textNodes.push(walker.currentNode); textNodes.forEach((node) => { let text = node.nodeValue; replacements.forEach(([from, to]) => { text = text.replaceAll(from, to); }); node.nodeValue = text; }); document.querySelectorAll("a, button, span").forEach((node) => { const label = node.getAttribute?.("aria-label"); if (label) { let next = label; replacements.forEach(([from, to]) => { next = next.replaceAll(from, to); }); node.setAttribute("aria-label", next); } const title = node.getAttribute?.("title"); if (title) { let next = title; replacements.forEach(([from, to]) => { next = next.replaceAll(from, to); }); node.setAttribute("title", next); } }); }; for (let i = 0; i < 20; i += 1) { setTimeout(normalizeFooter, i * 250); } new MutationObserver(normalizeFooter).observe(document.body, { childList: true, subtree: true, characterData: true, }); } """ def message_text(content) -> str: if isinstance(content, str): return content.strip() if isinstance(content, list): parts = [] for item in content: if isinstance(item, dict): text = item.get("text") if isinstance(text, str): parts.append(text) return " ".join(part.strip() for part in parts if part.strip()).strip() return "" def build_demo(respond_fn): """Build the NeuroBait chat. ``respond_fn`` signature (streaming generator ok): respond_fn(message: str, history: list[dict], mood: str) -> str | generator[str] """ with gr.Blocks(fill_height=True) as demo: gr.HTML(HEADER_HTML) mood = gr.Radio( MOODS, value="Calm", label="🌳 How are you feeling?", info="Pick the one that feels closest right now.", render=False, ) chatbot = gr.Chatbot( height=560, show_label=False, placeholder=PLACEHOLDER, avatar_images=(None, None), render=False, ) gr.ChatInterface( fn=respond_fn, chatbot=chatbot, additional_inputs=[mood], additional_inputs_accordion="🌳 How are you feeling?", title=TITLE, description=DESCRIPTION, examples=EXAMPLES, cache_examples=False, save_history=True, fill_height=True, autoscroll=True, ) gr.HTML(FOOTER_HTML) return demo