NeuroBait / ui.py
Haris-Subrata's picture
Upload folder using huggingface_hub
0751fa8 verified
Raw
History Blame
10.6 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 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"""
<header class="nb-header">
<img src="{MARK_SRC}" alt="" class="nb-mark" />
<div>
<div class="nb-wordmark">NeuroBait</div>
<div class="nb-kicker">task initiation companion</div>
</div>
</header>
"""
FOOTER_HTML = """
<div class="nb-footer-links">
<a href="/?view=api">Use via API</a>
<span>·</span>
<a href="https://gradio.app" target="_blank" rel="noreferrer">Built with Gradio</a>
<span>·</span>
<span>Settings</span>
</div>
"""
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