"""
Android Skill Router — Gradio demo for the Build Small hackathon.
Natural language in → fine-tuned Qwen2.5-3B intent out → Android UI trajectory.
Inference runs on Modal (deploy modal_apps/predict_api.py). Set MODAL_PREDICT_URL in Space secrets.
"""
from __future__ import annotations
import hashlib
import html
import json
import os
from pathlib import Path
import gradio as gr
import requests
from src.parameter_binder import apply_parameters
from src.skill_router import SKILL_TO_TRAJECTORY, load_trajectory
from src.skill_utils import resolve_skill
MODAL_PREDICT_URL = os.environ.get("MODAL_PREDICT_URL", "").rstrip("/")
PARAMETER_LABELS = {
"contact": "Contact",
"message": "Message",
"recipient": "Recipient",
"time": "Time",
"day": "Day",
"title": "Title",
"date": "Date",
"channel": "Channel",
"playlist": "Playlist",
"query": "Query",
"destination": "Destination",
"name": "Name",
}
EXAMPLE_PROMPTS = [
"play my workout playlist",
"turn bluetooth on",
"wake me up tomorrow morning",
"send ri a message on whatsapp",
"open the engineering channel in slack",
"find parag shah in contacts",
"email my team saying project update",
"pause spotify",
"book an uber to the airport",
"search pasta recipes on youtube",
]
SKILL_LABELS = {
"bluetooth_enable": "Turn on Bluetooth",
"calendar_create_event": "Create calendar event",
"camera_take_photo": "Take a photo",
"contacts_search": "Search contacts",
"create_alarm": "Set an alarm",
"gmail_send_email": "Send Gmail",
"linkedin_search_person": "Search LinkedIn",
"slack_open_channel": "Open Slack channel",
"spotify_pause": "Pause Spotify",
"spotify_play_playlist": "Play Spotify playlist",
"spotify_search_play": "Search & play on Spotify",
"uber_request_ride": "Request Uber ride",
"whatsapp_send_message": "Send WhatsApp message",
"wifi_enable": "Enable Wi-Fi",
"youtube_search": "Search YouTube",
}
FEATURED_SKILLS = [
("spotify_play_playlist", "Play Workout Playlist"),
("whatsapp_send_message", "Send WhatsApp Message"),
("create_alarm", "Create Alarm"),
("uber_request_ride", "Book Uber"),
("youtube_search", "Search YouTube"),
]
TOTAL_SKILLS = len(SKILL_TO_TRAJECTORY)
IDLE_RESULT_HTML = """
Describe what you want your phone to do — the model will extract the skill and parameters,
then surface the matching Android trajectory.
Detected Skill—
Extracted Parameters—
Predicted Confidence—
Android Trajectory—
"""
def _predict_intent(prompt: str) -> dict:
if not MODAL_PREDICT_URL:
raise gr.Error(
"MODAL_PREDICT_URL is not set. Deploy modal_apps/predict_api.py on Modal and add the "
"/predict endpoint URL as a Space secret."
)
response = requests.post(
f"{MODAL_PREDICT_URL}/predict",
json={"prompt": prompt},
timeout=120,
)
if response.status_code != 200:
try:
detail = response.json().get("message", response.text)
except Exception:
detail = response.text
raise gr.Error(f"Inference failed ({response.status_code}): {detail}")
payload = response.json()
raw_skill = payload.get("skill")
skill = resolve_skill(raw_skill if isinstance(raw_skill, str) else None, prompt.strip())
if not skill or skill not in SKILL_TO_TRAJECTORY:
raise gr.Error(
f"Could not route prompt to a known skill (model returned {raw_skill!r})."
)
parameters = payload.get("parameters", {})
if not isinstance(parameters, dict):
parameters = {}
return {"skill": skill, "parameters": parameters}
def _demo_confidence_percent(prompt: str, skill: str, parameters: dict | None = None) -> float:
"""Deterministic demo confidence for the hackathon UI."""
param_blob = json.dumps(parameters or {}, sort_keys=True, separators=(",", ":"))
digest = hashlib.sha256(
f"{prompt.strip().lower()}|{skill}|{param_blob}".encode()
).hexdigest()
return 94.0 + (int(digest[:4], 16) % 56) / 10.0
def _format_parameters_html(parameters: dict) -> str:
if not parameters:
return 'None required'
rows = []
for key, value in parameters.items():
label = PARAMETER_LABELS.get(key, key.replace("_", " ").title())
rows.append(
f''
f'{html.escape(label)}'
f'{html.escape(str(value))}'
f"
"
)
return "".join(rows)
def _build_result_card(skill: str, prompt: str, parameters: dict | None = None) -> str:
label = SKILL_LABELS.get(skill, skill.replace("_", " ").title())
params = parameters or {}
confidence = _demo_confidence_percent(prompt, skill, params)
data = load_trajectory(skill)
steps = data.get("steps", [])
task = html.escape(str(data.get("task", "—")))
app_pkg = html.escape(str(data.get("app", "—")))
trajectory_file = html.escape(Path(SKILL_TO_TRAJECTORY[skill]).name)
params_html = _format_parameters_html(params)
return f"""
Detected Skill
{html.escape(label)}
{html.escape(skill)}
Predicted Confidence
Greedy decode · fine-tuned Qwen2.5-3B
Extracted Parameters
{params_html}
Associated Android Trajectory
{task}
App · {app_pkg} ·
Steps · {len(steps)} UI snapshots ·
File · {trajectory_file}
"""
def _build_skills_catalog_card() -> str:
lines = [
f"{html.escape(skill)} — {html.escape(SKILL_LABELS.get(skill, skill))}"
for skill in sorted(SKILL_TO_TRAJECTORY)
]
return f"""
"""
def _trajectory_summary(skill: str, parameters: dict | None = None) -> tuple[str, str, str]:
data = load_trajectory(skill)
params = parameters or {}
if params:
data = apply_parameters(data, skill, params)
steps = data.get("steps", [])
task = data.get("task", "—")
app_pkg = data.get("app", "—")
trajectory_file = Path(SKILL_TO_TRAJECTORY[skill]).name
summary = (
f"### {SKILL_LABELS.get(skill, skill)}\n\n"
f"`{skill}`\n\n"
f"**Task** · {task} \n"
f"**App** · `{app_pkg}` \n"
f"**Steps** · {len(steps)} UI snapshots \n"
f"**File** · `{trajectory_file}`"
)
if params:
param_lines = " \n".join(f"**{k}** · {v}" for k, v in params.items())
summary += f"\n\n**Extracted parameters** \n{param_lines}"
summary += "\n\n**Trajectory preview** · runtime parameters substituted into recorded steps"
preview = {
"skill": skill,
"parameters": params,
"parameterized": bool(params),
"task": task,
"app": app_pkg,
"steps": len(steps),
"trajectory": SKILL_TO_TRAJECTORY[skill],
}
return summary, json.dumps(preview, indent=2), json.dumps(data, indent=2)[:12000]
def route_prompt(prompt: str) -> tuple[str, gr.update, str]:
prompt = prompt.strip()
if not prompt:
raise gr.Error("Enter a prompt to classify.")
intent = _predict_intent(prompt)
skill = intent["skill"]
parameters = intent.get("parameters", {})
_, preview_json, trajectory_json = _trajectory_summary(skill, parameters)
return (
_build_result_card(skill, prompt, parameters),
gr.update(value=preview_json, visible=True),
trajectory_json,
)
def list_skills() -> tuple[str, gr.update, str]:
return _build_skills_catalog_card(), gr.update(value="", visible=False), ""
MAGENTA = "#E6007A"
NAVY = "#002D62"
NAVY_DEEP = "#001F3F"
SURFACE = "#FFFFFF"
SURFACE_MUTED = "#F4F7FB"
TEXT_MUTED = "#4A6278"
yoyo_theme = (
gr.themes.Base(
primary_hue="blue",
secondary_hue="pink",
neutral_hue="slate",
font=gr.themes.GoogleFont("DM Sans"),
font_mono=gr.themes.GoogleFont("JetBrains Mono"),
)
.set(
body_background_fill="transparent",
body_background_fill_dark="transparent",
body_text_color=NAVY,
body_text_color_dark=NAVY,
block_background_fill=SURFACE,
block_background_fill_dark=SURFACE,
block_border_width="0px",
block_label_text_color=TEXT_MUTED,
block_label_text_color_dark=TEXT_MUTED,
block_title_text_color=NAVY,
block_title_text_color_dark=NAVY,
block_radius="16px",
button_large_radius="999px",
button_primary_background_fill=NAVY,
button_primary_background_fill_hover=NAVY_DEEP,
button_primary_text_color="#FFFFFF",
button_primary_text_color_dark="#FFFFFF",
button_secondary_background_fill=SURFACE,
button_secondary_background_fill_dark=SURFACE,
button_secondary_text_color=NAVY,
button_secondary_text_color_dark=NAVY,
button_secondary_background_fill_hover=SURFACE_MUTED,
button_secondary_background_fill_hover_dark=SURFACE_MUTED,
input_background_fill=SURFACE,
input_background_fill_dark=SURFACE,
input_border_color="rgba(0,45,98,0.16)",
input_border_color_dark="rgba(0,45,98,0.16)",
input_border_color_focus=NAVY,
input_border_color_focus_dark=NAVY,
input_radius="14px",
checkbox_label_text_color=NAVY,
link_text_color=MAGENTA,
link_text_color_hover=NAVY_DEEP,
shadow_drop="0 12px 40px rgba(0,31,63,0.08)",
)
)
APP_CSS = """
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&family=Press+Start+2P&display=swap');
:root {
--magenta: #E6007A;
--magenta-soft: rgba(230, 0, 122, 0.10);
--navy: #002D62;
--navy-deep: #001F3F;
--text-muted: #4A6278;
--card: #FFFFFF;
--card-border: rgba(0, 45, 98, 0.10);
--surface-muted: #F4F7FB;
}
.gradio-container {
background:
radial-gradient(ellipse 90% 55% at 50% -10%, rgba(230,0,122,0.10) 0%, transparent 55%),
radial-gradient(ellipse 50% 40% at 100% 100%, rgba(0,45,98,0.06) 0%, transparent 50%),
linear-gradient(180deg, #FFF8FB 0%, #F7FAFC 55%, #EEF2F7 100%) !important;
color: var(--navy) !important;
max-width: 1180px !important;
margin: 0 auto !important;
padding: 2.5rem 1.25rem 3rem !important;
font-family: 'DM Sans', system-ui, sans-serif !important;
}
footer { display: none !important; }
.hero {
text-align: center;
margin-bottom: 1.75rem;
animation: fade-up 0.6s ease-out;
}
.hero-badge {
display: inline-block;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--magenta);
background: var(--magenta-soft);
border: 1px solid rgba(230,0,122,0.18);
border-radius: 999px;
padding: 0.35rem 0.9rem;
margin-bottom: 1rem;
}
.hero h1 {
font-family: 'Press Start 2P', monospace;
font-size: clamp(1rem, 3vw, 1.35rem);
line-height: 1.65;
color: var(--navy-deep);
margin: 0 0 0.85rem;
text-shadow: 2px 2px 0 rgba(230,0,122,0.12);
}
.hero .subtitle {
color: var(--text-muted);
max-width: 36rem;
margin: 0 auto 1.35rem;
font-size: 1.08rem;
line-height: 1.65;
font-weight: 500;
}
.pipeline {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
margin: 0 auto;
padding: 1rem 1.5rem;
background: var(--card);
border: 1px solid var(--card-border);
border-radius: 18px;
box-shadow: 0 8px 28px rgba(0,31,63,0.06);
}
.pipeline-step {
font-family: 'Press Start 2P', monospace;
font-size: clamp(0.42rem, 1.1vw, 0.52rem);
line-height: 1.8;
color: var(--navy-deep);
background: var(--surface-muted);
border: 1px solid rgba(0,45,98,0.10);
border-radius: 10px;
padding: 0.45rem 1rem;
min-width: 14rem;
text-align: center;
}
.pipeline-step.highlight {
background: var(--magenta-soft);
border-color: rgba(230,0,122,0.22);
color: var(--navy-deep);
}
.pipeline-arrow {
color: var(--magenta);
font-size: 1rem;
font-weight: 700;
line-height: 1;
user-select: none;
}
.content-row { gap: 1rem !important; align-items: stretch !important; }
.skills-learned-card {
background: var(--card);
border: 1px solid var(--card-border);
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0,31,63,0.07);
padding: 1.15rem 1.25rem;
height: 100%;
animation: fade-up 0.7s ease-out;
}
.skills-learned-card .card-title {
font-family: 'Press Start 2P', monospace;
font-size: 0.52rem;
line-height: 1.7;
color: var(--navy-deep);
margin: 0 0 0.85rem;
text-transform: uppercase;
}
.skills-learned-card ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.skills-learned-card li {
display: flex;
align-items: center;
gap: 0.55rem;
font-size: 0.88rem;
font-weight: 600;
color: var(--navy-deep);
background: var(--surface-muted);
border: 1px solid rgba(0,45,98,0.08);
border-radius: 12px;
padding: 0.5rem 0.65rem;
}
.skills-learned-card li::before {
content: "▸";
color: var(--magenta);
font-size: 0.75rem;
flex-shrink: 0;
}
.skills-learned-card .skills-count {
margin-top: 0.85rem;
padding-top: 0.85rem;
border-top: 1px solid rgba(0,45,98,0.08);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--magenta);
}
.main-stack { gap: 1rem !important; }
.glass-panel {
background: var(--card) !important;
border: 1px solid var(--card-border) !important;
border-radius: 20px !important;
box-shadow: 0 8px 32px rgba(0,31,63,0.07), 0 1px 3px rgba(0,31,63,0.04) !important;
padding: 1.25rem 1.35rem !important;
min-height: 100%;
}
.glass-panel > .wrap,
.glass-panel.wrap {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
}
.panel-label {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-muted);
margin: 0 0 0.85rem 0.15rem;
}
.prompt-input textarea,
.prompt-input input {
background: var(--card) !important;
color: var(--navy-deep) !important;
font-size: 1.05rem !important;
line-height: 1.5 !important;
border-radius: 14px !important;
border: 1px solid rgba(0,45,98,0.14) !important;
box-shadow: inset 0 1px 2px rgba(0,31,63,0.03) !important;
}
.prompt-input textarea::placeholder,
.prompt-input input::placeholder {
color: #8A9BB0 !important;
opacity: 1 !important;
}
.prompt-input textarea:focus,
.prompt-input input:focus {
border-color: var(--navy) !important;
box-shadow: 0 0 0 3px rgba(0,45,98,0.10) !important;
}
.btn-row { gap: 0.6rem !important; margin-top: 0.25rem !important; }
.btn-row button {
border-radius: 999px !important;
font-weight: 600 !important;
letter-spacing: 0.01em !important;
padding: 0.65rem 1.4rem !important;
transition: transform 0.15s ease, box-shadow 0.15s ease !important;
}
.btn-row button:hover { transform: translateY(-1px); }
.btn-row button.primary {
background: var(--navy) !important;
color: #FFFFFF !important;
border: none !important;
box-shadow: 0 4px 14px rgba(0,31,63,0.18) !important;
}
.btn-row button.primary:hover {
background: var(--navy-deep) !important;
}
.btn-row button.secondary {
background: var(--card) !important;
color: var(--navy) !important;
border: 1px solid rgba(0,45,98,0.16) !important;
}
.btn-row button.secondary:hover {
background: var(--surface-muted) !important;
}
.examples-wrap,
.examples-wrap > .wrap,
.examples-wrap .block {
background: var(--surface-muted) !important;
border: 1px solid rgba(0,45,98,0.08) !important;
border-radius: 14px !important;
padding: 0.65rem 0.75rem !important;
margin-top: 0.75rem !important;
}
.examples-wrap .label-wrap {
display: block !important;
margin-bottom: 0.5rem !important;
}
.examples-wrap .label-wrap span,
.examples-wrap label span {
color: var(--text-muted) !important;
font-size: 0.72rem !important;
font-weight: 700 !important;
letter-spacing: 0.12em !important;
text-transform: uppercase !important;
}
.examples-wrap table { border: none !important; width: 100% !important; }
.examples-wrap tbody tr td {
background: var(--card) !important;
border: 1px solid rgba(0,45,98,0.12) !important;
border-radius: 999px !important;
padding: 0.4rem 0.9rem !important;
margin: 0.25rem !important;
font-size: 0.84rem !important;
font-weight: 500 !important;
color: var(--navy-deep) !important;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease !important;
}
.examples-wrap tbody tr td:hover {
background: #FFFFFF !important;
border-color: var(--magenta) !important;
transform: translateY(-1px);
cursor: pointer;
}
.result-card-wrap .block,
.result-card-wrap .html-container {
padding: 0 !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
.result-card {
background: var(--card);
border: 1px solid var(--card-border);
border-radius: 18px;
padding: 1.15rem 1.25rem;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.8);
}
.result-card--idle {
background: linear-gradient(180deg, var(--card) 0%, var(--surface-muted) 100%);
}
.result-card-header {
font-family: 'Press Start 2P', monospace;
font-size: 0.5rem;
line-height: 1.7;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--navy-deep);
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(0,45,98,0.08);
}
.result-placeholder {
color: var(--text-muted);
font-size: 0.92rem;
line-height: 1.65;
margin: 0 0 1rem;
}
.result-metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.result-metrics--empty .metric-tile {
background: rgba(255,255,255,0.65);
}
.metric-tile {
background: var(--surface-muted);
border: 1px solid rgba(0,45,98,0.08);
border-radius: 14px;
padding: 0.85rem 0.95rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.metric-tile--wide { grid-column: 1 / -1; }
.metric-label {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
}
.metric-value {
font-size: 1rem;
font-weight: 700;
color: var(--navy-deep);
line-height: 1.35;
}
.metric-value.muted {
color: #8A9BB0;
font-weight: 600;
}
.metric-sub {
font-size: 0.8rem;
color: var(--text-muted);
line-height: 1.5;
}
.metric-sub code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
background: rgba(0,45,98,0.06);
padding: 0.1em 0.35em;
border-radius: 5px;
color: var(--navy-deep);
}
.confidence-row {
display: flex;
align-items: center;
gap: 0.65rem;
}
.confidence-bar {
flex: 1;
height: 10px;
background: rgba(0,45,98,0.08);
border-radius: 999px;
overflow: hidden;
}
.confidence-fill {
height: 100%;
background: linear-gradient(90deg, var(--navy) 0%, var(--magenta) 100%);
border-radius: 999px;
transition: width 0.4s ease;
}
.confidence-pct {
font-family: 'JetBrains Mono', monospace;
font-size: 0.95rem;
font-weight: 700;
color: var(--navy-deep);
min-width: 3.5rem;
text-align: right;
}
.param-list {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.param-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.35rem 0.65rem;
background: rgba(255,255,255,0.7);
border: 1px solid rgba(0,45,98,0.08);
border-radius: 10px;
padding: 0.45rem 0.65rem;
}
.param-key {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--magenta);
}
.param-value {
font-size: 0.92rem;
font-weight: 600;
color: var(--navy-deep);
line-height: 1.4;
}
.skills-catalog {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
max-height: 22rem;
overflow-y: auto;
}
.skills-catalog li {
font-size: 0.84rem;
color: var(--navy-deep);
background: var(--surface-muted);
border: 1px solid rgba(0,45,98,0.08);
border-radius: 10px;
padding: 0.45rem 0.65rem;
}
.skills-catalog code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
color: var(--magenta);
}
.result-md .prose {
color: var(--navy-deep) !important;
font-size: 0.95rem !important;
line-height: 1.7 !important;
}
.result-md .prose em,
.result-md .prose p {
color: var(--text-muted) !important;
}
.result-md .prose code {
background: var(--magenta-soft) !important;
color: var(--navy-deep) !important;
border-radius: 6px !important;
padding: 0.1em 0.35em !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 0.88em !important;
}
.result-md .prose strong { color: var(--navy-deep) !important; }
.result-md .prose h3 { color: var(--navy-deep) !important; }
.code-block,
.code-block .wrap {
background: var(--surface-muted) !important;
}
.code-block pre,
.code-block code {
background: var(--surface-muted) !important;
color: var(--navy-deep) !important;
border-radius: 12px !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 0.8rem !important;
border: 1px solid rgba(0,45,98,0.10) !important;
}
.code-block label span {
color: var(--text-muted) !important;
font-weight: 600 !important;
}
.accordion-wrap {
background: var(--surface-muted) !important;
border: 1px solid rgba(0,45,98,0.10) !important;
border-radius: 14px !important;
overflow: hidden;
margin-top: 0.75rem !important;
}
.accordion-wrap > .label-wrap {
background: var(--surface-muted) !important;
border-bottom: 1px solid rgba(0,45,98,0.08) !important;
}
.accordion-wrap button,
.accordion-wrap .label-wrap span {
color: var(--navy) !important;
font-weight: 600 !important;
font-size: 0.9rem !important;
}
.accordion-wrap svg {
color: var(--magenta) !important;
stroke: var(--magenta) !important;
}
.site-footer {
margin-top: 2rem;
padding: 1.25rem 1.5rem;
background: var(--card);
border: 1px solid var(--card-border);
border-radius: 16px;
text-align: center;
animation: fade-up 0.8s ease-out;
}
.site-footer .footer-title {
font-family: 'Press Start 2P', monospace;
font-size: clamp(0.42rem, 1.1vw, 0.5rem);
line-height: 1.8;
color: var(--navy-deep);
margin: 0 0 0.65rem;
}
.site-footer .footer-meta {
display: flex;
flex-wrap: wrap;
gap: 0.65rem 1.25rem;
justify-content: center;
align-items: center;
font-size: 0.82rem;
color: var(--text-muted);
}
.site-footer a {
color: var(--magenta);
font-weight: 600;
text-decoration: none;
border-bottom: 1px solid rgba(230,0,122,0.35);
}
.site-footer a:hover { border-bottom-color: var(--navy-deep); color: var(--navy-deep); }
.footer-pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: var(--surface-muted);
border-radius: 999px;
padding: 0.3rem 0.75rem;
font-weight: 600;
color: var(--navy);
}
.footer-pill.ok { color: #0a6b3e; background: rgba(10,107,62,0.08); }
.footer-pill.warn { color: #8a4b00; background: rgba(138,75,0,0.08); }
/* Force readable light surfaces even when Gradio dark mode is active */
.dark .gradio-container {
background:
radial-gradient(ellipse 90% 55% at 50% -10%, rgba(230,0,122,0.12) 0%, transparent 55%),
radial-gradient(ellipse 50% 40% at 100% 100%, rgba(0,45,98,0.06) 0%, transparent 50%),
linear-gradient(180deg, #FFF8FB 0%, #F7FAFC 55%, #EEF2F7 100%) !important;
color: var(--navy) !important;
}
.dark .glass-panel,
.dark .glass-panel > .wrap {
background: var(--card) !important;
color: var(--navy-deep) !important;
}
.dark .prompt-input textarea,
.dark .prompt-input input {
background: var(--card) !important;
color: var(--navy-deep) !important;
border-color: rgba(0,45,98,0.14) !important;
}
.dark .examples-wrap,
.dark .examples-wrap > .wrap,
.dark .examples-wrap .block {
background: var(--surface-muted) !important;
}
.dark .examples-wrap tbody tr td {
background: var(--card) !important;
color: var(--navy-deep) !important;
}
.dark .accordion-wrap,
.dark .accordion-wrap > .label-wrap {
background: var(--surface-muted) !important;
}
.dark .accordion-wrap button,
.dark .accordion-wrap .label-wrap span {
color: var(--navy) !important;
}
.dark .code-block pre,
.dark .code-block code {
background: var(--surface-muted) !important;
color: var(--navy-deep) !important;
}
.dark .btn-row button.primary {
background: var(--navy) !important;
color: #FFFFFF !important;
}
.dark .btn-row button.secondary {
background: var(--card) !important;
color: var(--navy) !important;
}
.dark .result-md .prose,
.dark .result-md .prose p,
.dark .result-md .prose h3 {
color: var(--navy-deep) !important;
}
.dark .result-md .prose em {
color: var(--text-muted) !important;
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.gradio-container { padding: 1.5rem 0.85rem 2rem !important; }
.result-metrics { grid-template-columns: 1fr; }
.pipeline-step { min-width: 11rem; font-size: 0.4rem; }
}
"""
INFERENCE_STATUS = (
''
if MODAL_PREDICT_URL
else ''
)
SKILLS_LEARNED_HTML = """
Skills Learned
{total} skills learned
""".format(
items="\n".join(f" {html.escape(label)}" for _, label in FEATURED_SKILLS),
total=TOTAL_SKILLS,
)
with gr.Blocks(
title="Pocket Automator",
theme=yoyo_theme,
css=APP_CSS,
) as demo:
gr.HTML(
"""
AI-Powered Android Automation
Pocket Automator
Teach your phone new skills through demonstration.
Voice / Text
↓
Qwen 2.5 3B
↓
Intent Extraction
↓
Replay Engine
↓
Android Action
"""
)
with gr.Row(elem_classes=["content-row"]):
with gr.Column(scale=3, min_width=240):
gr.HTML(SKILLS_LEARNED_HTML)
with gr.Column(scale=9):
with gr.Row(equal_height=False, elem_classes=["main-stack"]):
with gr.Column(scale=5, elem_classes=["glass-panel"]):
gr.HTML('Tell your phone what to do
')
prompt = gr.Textbox(
label="What do you want to do?",
placeholder="play my chill playlist on spotify",
lines=3,
elem_classes=["prompt-input"],
show_label=False,
)
with gr.Row(elem_classes=["btn-row"]):
submit = gr.Button("Run intent →", variant="primary", scale=2)
skills_btn = gr.Button("Browse skills", variant="secondary", scale=1)
with gr.Group(elem_classes=["examples-wrap"]):
gr.Examples(
examples=[[p] for p in EXAMPLE_PROMPTS],
inputs=prompt,
label="Try an example",
)
with gr.Column(scale=6, elem_classes=["glass-panel"]):
gr.HTML('Live automation result
')
result_card = gr.HTML(
value=IDLE_RESULT_HTML,
elem_classes=["result-card-wrap"],
show_label=False,
)
preview_json = gr.Code(
label="Intent + routing metadata",
language="json",
elem_classes=["code-block"],
visible=False,
)
with gr.Accordion(
"Trajectory preview",
open=False,
elem_classes=["accordion-wrap"],
):
trajectory_json = gr.Code(
label="Trajectory JSON (truncated)",
language="json",
lines=16,
elem_classes=["code-block"],
show_label=False,
)
gr.HTML(
f"""
"""
)
submit.click(route_prompt, inputs=prompt, outputs=[result_card, preview_json, trajectory_json])
skills_btn.click(list_skills, outputs=[result_card, preview_json, trajectory_json])
if __name__ == "__main__":
demo.launch()