Spaces:
Running on Zero
Running on Zero
| """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, | |
| ) | |