amigo / assistant /ui.py
Jose Esparza
First up
46bd2ee unverified
Raw
History Blame Contribute Delete
6.86 kB
from __future__ import annotations
import gradio as gr
from config import CONFIG
THEME = gr.themes.Soft(
primary_hue="orange",
secondary_hue="emerald",
neutral_hue="stone",
text_size=gr.themes.sizes.text_lg,
spacing_size=gr.themes.sizes.spacing_lg,
radius_size=gr.themes.sizes.radius_lg,
font=[gr.themes.GoogleFont("Atkinson Hyperlegible"), "system-ui", "sans-serif"],
).set(
body_background_fill="linear-gradient(160deg, #fdf6ec 0%, #f6e7d2 100%)",
body_text_color="#3d352e",
block_background_fill="#fffdf9",
block_border_width="0px",
block_shadow="0 10px 34px rgba(61, 53, 46, 0.08)",
block_radius="26px",
button_primary_background_fill="#e07a5f",
button_primary_background_fill_hover="#cf6a4f",
button_primary_text_color="#ffffff",
button_large_radius="40px",
)
CSS = """
.gradio-container { max-width: 760px !important; margin: 0 auto !important; }
footer, .footer { display: none !important; }
/* Header */
#amigo-head { text-align: center; margin: 8px 0 2px; }
#amigo-head h1 { font-size: 40px; margin: 0; color: #3d352e; }
#amigo-head p { font-size: 22px; margin: 4px 0 0; color: #8a7a68; }
/* Reactive avatar */
.amigo-avatar { display: flex; flex-direction: column; align-items: center;
gap: 14px; padding: 18px 0 6px; }
.amigo-avatar .orb {
width: 150px; height: 150px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 76px; background: radial-gradient(circle at 35% 30%, #ffe6c7, #e9b48a);
box-shadow: 0 12px 30px rgba(224, 122, 95, 0.30);
transition: transform .25s ease, box-shadow .25s ease; }
.amigo-avatar .cap { font-size: 24px; color: #6b5d4d; font-weight: 600; }
/* per-state colors + motion */
.amigo-avatar.is-listening .orb {
background: radial-gradient(circle at 35% 30%, #d7efe2, #81b29a);
animation: pulse 1.2s ease-in-out infinite; }
.amigo-avatar.is-thinking .orb {
background: radial-gradient(circle at 35% 30%, #fdeccb, #e6c684);
animation: sway 1.6s ease-in-out infinite; }
.amigo-avatar.is-talking .orb {
background: radial-gradient(circle at 35% 30%, #ffd9c4, #e07a5f);
animation: pulse .7s ease-in-out infinite; }
@keyframes pulse {
0%,100% { transform: scale(1); box-shadow: 0 12px 30px rgba(224,122,95,.30); }
50% { transform: scale(1.07); box-shadow: 0 16px 40px rgba(224,122,95,.45); } }
@keyframes sway {
0%,100% { transform: rotate(-4deg); } 50% { transform: rotate(4deg); } }
/* Chat bubbles: big and warm */
#amigo-chat .message { font-size: 23px !important; line-height: 1.55 !important;
border-radius: 22px !important; padding: 14px 18px !important; }
#amigo-chat .message.user { background: #e07a5f !important; color: #fff !important; }
#amigo-chat .message.bot { background: #f1e7d6 !important; color: #3d352e !important; }
/* Tap-to-talk: turn Gradio's audio widget into one big round button.
The avatar already says what to do, so we hide every other control. */
#amigo-mic { border: none !important; background: transparent !important;
box-shadow: none !important; }
#amigo-mic [data-testid="block-label"],
#amigo-mic .icon-button-wrapper,
#amigo-mic .pause-button,
#amigo-mic .resume-button,
#amigo-mic .mic-select,
#amigo-mic .microphone,
#amigo-mic [data-testid="recording-waveform"],
#amigo-mic [data-testid="status-tracker"] { display: none !important; }
#amigo-mic .controls, #amigo-mic .controls .wrapper {
display: flex !important; justify-content: center !important;
align-items: center !important; border: none !important; background: none !important; }
/* The icon is a white SVG background (a mic / a square), which `!important`
wins reliably. We hide the button's text and both pseudo-elements, so none
of Gradio's record-dot or waveform marks leak through. We never touch
`display`, so Gradio's own show/hide of record-vs-stop keeps working. */
#amigo-mic .record-button > *,
#amigo-mic .record-button::before, #amigo-mic .record-button::after,
#amigo-mic .stop-button > *, #amigo-mic .stop-button-paused > *,
#amigo-mic .stop-button::before, #amigo-mic .stop-button::after,
#amigo-mic .stop-button-paused::before, #amigo-mic .stop-button-paused::after {
display: none !important; }
/* idle: a big green "talk" circle with a white mic */
#amigo-mic .record-button {
width: 150px !important; height: 150px !important; border-radius: 50% !important;
border: none !important; color: transparent !important; font-size: 0 !important;
background-color: #81b29a !important;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3z'/%3E%3Cpath d='M19 12a7 7 0 0 1-14 0H3a9 9 0 0 0 8 8.94V23h2v-2.06A9 9 0 0 0 21 12z'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important; background-position: center !important;
background-size: 62px 62px !important;
box-shadow: 0 10px 28px rgba(129, 178, 154, .45) !important;
transition: transform .15s ease; }
#amigo-mic .record-button:hover { transform: scale(1.05); }
/* recording: a big red, pulsing "stop" circle with a white square */
#amigo-mic .stop-button, #amigo-mic .stop-button-paused {
width: 150px !important; height: 150px !important; border-radius: 50% !important;
border: none !important; color: transparent !important; font-size: 0 !important;
background-color: #e07a5f !important;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Crect x='7' y='7' width='10' height='10' rx='2'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important; background-position: center !important;
background-size: 58px 58px !important;
box-shadow: 0 10px 28px rgba(224, 122, 95, .5) !important;
animation: pulse .9s ease-in-out infinite; }
/* Transcript: a soft reading aid, not a stark white void */
#amigo-chat { background: #fffdf9 !important; border-radius: 22px !important; }
/* Reply player: present (so autoplay works) but small and quiet */
#amigo-reply { opacity: .5; transform: scale(.85); }
#amigo-reply label { font-size: 15px !important; }
"""
# CSS class per state (language-independent); icon and caption come from the pack.
_CLASS = {
"idle": "", "listening": "is-listening",
"thinking": "is-thinking", "talking": "is-talking",
}
def avatar(state: str, caption: str | None = None) -> str:
"""Render the avatar orb HTML for a turn state, with an optional caption."""
icon, default_caption = CONFIG.pack["ui"]["states"].get(
state, CONFIG.pack["ui"]["states"]["idle"]
)
text = caption or default_caption
return (
f'<div class="amigo-avatar {_CLASS.get(state, "")}">'
f'<div class="orb">{icon}</div>'
f'<div class="cap">{text}</div></div>'
)