| """ |
| 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 = """ |
| <div class="result-card result-card--idle"> |
| <div class="result-card-header">Automation Result</div> |
| <p class="result-placeholder"> |
| Describe what you want your phone to do β the model will extract the skill and parameters, |
| then surface the matching Android trajectory. |
| </p> |
| <div class="result-metrics result-metrics--empty"> |
| <div class="metric-tile"><span class="metric-label">Detected Skill</span><span class="metric-value muted">β</span></div> |
| <div class="metric-tile"><span class="metric-label">Extracted Parameters</span><span class="metric-value muted">β</span></div> |
| <div class="metric-tile"><span class="metric-label">Predicted Confidence</span><span class="metric-value muted">β</span></div> |
| <div class="metric-tile metric-tile--wide"><span class="metric-label">Android Trajectory</span><span class="metric-value muted">β</span></div> |
| </div> |
| </div> |
| """ |
|
|
|
|
| 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 '<span class="metric-value muted">None required</span>' |
|
|
| rows = [] |
| for key, value in parameters.items(): |
| label = PARAMETER_LABELS.get(key, key.replace("_", " ").title()) |
| rows.append( |
| f'<div class="param-row">' |
| f'<span class="param-key">{html.escape(label)}</span>' |
| f'<span class="param-value">{html.escape(str(value))}</span>' |
| f"</div>" |
| ) |
| 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""" |
| <div class="result-card"> |
| <div class="result-card-header">Automation Result</div> |
| <div class="result-metrics"> |
| <div class="metric-tile"> |
| <span class="metric-label">Detected Skill</span> |
| <span class="metric-value">{html.escape(label)}</span> |
| <span class="metric-sub"><code>{html.escape(skill)}</code></span> |
| </div> |
| <div class="metric-tile"> |
| <span class="metric-label">Predicted Confidence</span> |
| <div class="confidence-row"> |
| <div class="confidence-bar" role="progressbar" aria-valuenow="{confidence:.1f}" aria-valuemin="0" aria-valuemax="100"> |
| <div class="confidence-fill" style="width: {confidence:.1f}%;"></div> |
| </div> |
| <span class="confidence-pct">{confidence:.1f}%</span> |
| </div> |
| <span class="metric-sub">Greedy decode Β· fine-tuned Qwen2.5-3B</span> |
| </div> |
| <div class="metric-tile metric-tile--wide"> |
| <span class="metric-label">Extracted Parameters</span> |
| <div class="param-list">{params_html}</div> |
| </div> |
| <div class="metric-tile metric-tile--wide"> |
| <span class="metric-label">Associated Android Trajectory</span> |
| <span class="metric-value">{task}</span> |
| <span class="metric-sub"> |
| <strong>App</strong> Β· <code>{app_pkg}</code> Β· |
| <strong>Steps</strong> Β· {len(steps)} UI snapshots Β· |
| <strong>File</strong> Β· <code>{trajectory_file}</code> |
| </span> |
| </div> |
| </div> |
| </div> |
| """ |
|
|
|
|
| def _build_skills_catalog_card() -> str: |
| lines = [ |
| f"<li><code>{html.escape(skill)}</code> β {html.escape(SKILL_LABELS.get(skill, skill))}</li>" |
| for skill in sorted(SKILL_TO_TRAJECTORY) |
| ] |
| return f""" |
| <div class="result-card"> |
| <div class="result-card-header">All Learned Skills</div> |
| <ul class="skills-catalog">{''.join(lines)}</ul> |
| </div> |
| """ |
|
|
|
|
| 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 = ( |
| '<span class="footer-pill ok">β inference live</span>' |
| if MODAL_PREDICT_URL |
| else '<span class="footer-pill warn">β set MODAL_PREDICT_URL</span>' |
| ) |
|
|
| SKILLS_LEARNED_HTML = """ |
| <div class="skills-learned-card"> |
| <div class="card-title">Skills Learned</div> |
| <ul> |
| {items} |
| </ul> |
| <div class="skills-count">{total} skills learned</div> |
| </div> |
| """.format( |
| items="\n".join(f" <li>{html.escape(label)}</li>" for _, label in FEATURED_SKILLS), |
| total=TOTAL_SKILLS, |
| ) |
|
|
| with gr.Blocks( |
| title="Pocket Automator", |
| theme=yoyo_theme, |
| css=APP_CSS, |
| ) as demo: |
| gr.HTML( |
| """ |
| <div class="hero"> |
| <div class="hero-badge">AI-Powered Android Automation</div> |
| <h1>Pocket Automator</h1> |
| <p class="subtitle">Teach your phone new skills through demonstration.</p> |
| <div class="pipeline"> |
| <span class="pipeline-step">Voice / Text</span> |
| <span class="pipeline-arrow">β</span> |
| <span class="pipeline-step highlight">Qwen 2.5 3B</span> |
| <span class="pipeline-arrow">β</span> |
| <span class="pipeline-step">Intent Extraction</span> |
| <span class="pipeline-arrow">β</span> |
| <span class="pipeline-step">Replay Engine</span> |
| <span class="pipeline-arrow">β</span> |
| <span class="pipeline-step">Android Action</span> |
| </div> |
| </div> |
| """ |
| ) |
|
|
| 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('<p class="panel-label">Tell your phone what to do</p>') |
| 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('<p class="panel-label">Live automation result</p>') |
| 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""" |
| <div class="site-footer"> |
| <p class="footer-title">Built for the Hugging Face Build Small Hackathon</p> |
| <div class="footer-meta"> |
| <span class="footer-pill">Qwen2.5-3B Β· QLoRA</span> |
| {INFERENCE_STATUS} |
| <a href="https://huggingface.co/Qwen/Qwen2.5-3B-Instruct" target="_blank">model card</a> |
| <span>Β·</span> |
| <span>{TOTAL_SKILLS} Android skills</span> |
| </div> |
| </div> |
| """ |
| ) |
|
|
| 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() |
|
|