NeuroBait / ui.py
Haris-Subrata's picture
Upload folder using huggingface_hub
97aa0c1 verified
Raw
History Blame
7.44 kB
"""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 gradio as gr
MOODS = ["Calm", "Tired", "Anxious", "Focused"]
THEME = gr.themes.Soft(primary_hue="teal", neutral_hue="slate")
TITLE = "🧠 NeuroBait"
DESCRIPTION = (
"A warm space and a gentle boost for your everyday. "
"No red pen · no urgency · no streaks."
)
# 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 = gr.I18n(
en={
"use_via_api": "Use via API",
"built_with_gradio": "Built with Gradio",
"settings": "Settings",
}
)
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; }
/* 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);
}
"""
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]
"""
mood = gr.Radio(
MOODS,
value="Calm",
label="🌳 How are you feeling?",
info="Pick the one that feels closest right now.",
)
chatbot = gr.Chatbot(
height=560,
show_label=False,
placeholder=PLACEHOLDER,
avatar_images=(None, None),
)
return gr.ChatInterface(
fn=respond_fn,
chatbot=chatbot,
additional_inputs=[mood],
additional_inputs_accordion=gr.Accordion("🌳 How are you feeling?", open=True),
title=TITLE,
description=DESCRIPTION,
examples=EXAMPLES,
cache_examples=False,
save_history=True,
fill_height=True,
autoscroll=True,
)