Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

684 lines
46 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MobileGym: A Verifiable and Highly Parallel Simulation Platform for Mobile GUI Agent Research</title>
<meta name="description" content="MobileGym is a verifiable and highly parallel simulation platform for mobile GUI agent research — programmatic state verification, structured state snapshots, identical-state replication, and scalable online RL across 28 mobile apps." />
<link rel="canonical" href="https://mobilegym.dev/paper/" />
<meta property="og:title" content="MobileGym: A Verifiable and Highly Parallel Simulation Platform for Mobile GUI Agent Research" />
<meta property="og:description" content="Structured environment state enables verifiable evaluation (vs. 10.2% VLM-judge misjudgment) and scalable online training (single-machine batch-parallel GRPO). 28 apps, 416 parameterized task templates, +40.7 pt Sim-to-Real." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://mobilegym.dev/paper/" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="MobileGym: A Verifiable and Highly Parallel Simulation Platform for Mobile GUI Agent Research" />
<meta name="twitter:description" content="28 apps, 416 parameterized task templates, programmatic state verification, scalable online RL, +40.7 pt Sim-to-Real." />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%F0%9F%93%B1%3C/text%3E%3C/svg%3E" />
<link rel="stylesheet" href="tailwind.css" />
<link rel="stylesheet" href="styles.css" />
<script defer src="scripts/state-builder.js"></script>
<script defer src="scripts/boot-hero.js"></script>
</head>
<body class="text-stone-900 gradient-bg">
<section id="demo" class="hero-demo scroll-mt-8" aria-label="MobileGym live simulator">
<div class="hero-demo-brand">
<span class="hero-demo-brand-name">MobileGym</span>
<span class="hero-demo-brand-meta">Live demo</span>
</div>
<div class="hero-demo-actions">
<a href="https://arxiv.org/abs/2605.26114" target="_blank" rel="noopener"
class="hero-demo-action hero-demo-action-arxiv" aria-label="Read paper on arXiv">
<span class="hero-demo-arxiv-mark">arXiv</span>
</a>
<a href="https://github.com/Purewhiter/mobilegym" target="_blank" rel="noopener"
class="hero-demo-action" aria-label="View source on GitHub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.4 3-.405 1.02.005 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
</a>
<button type="button" id="demo-poweroff-btn" class="hero-demo-poweroff hidden">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>
<span>Power off</span>
</button>
</div>
<div class="demo-layout ui">
<div class="demo-phone-wrap">
<!-- 现代全面屏:iframe 尺寸严格匹配 simulator 原生 360×800 设计视口,
避免 simulator 的 #root 居中后产生黑色边距 -->
<div class="phone-rig relative" style="width: 360px; height: 800px;">
<!-- 侧边按钮(极简装饰) -->
<span class="absolute -right-[1.5px] top-[170px] w-[2px] h-[74px] rounded-r-sm pointer-events-none"
style="background: #2a2c32;"></span>
<span class="absolute -left-[1.5px] top-[132px] w-[2px] h-[32px] rounded-l-sm pointer-events-none"
style="background: #2a2c32;"></span>
<span class="absolute -left-[1.5px] top-[185px] w-[2px] h-[55px] rounded-l-sm pointer-events-none"
style="background: #2a2c32;"></span>
<span class="absolute -left-[1.5px] top-[254px] w-[2px] h-[55px] rounded-l-sm pointer-events-none"
style="background: #2a2c32;"></span>
<!-- 手机壳 = 屏幕本身(圆角直接裁屏幕内容,无 padding bezel)。
box-shadow 移到 CSS 里以便在 hero pre-boot 状态下隐藏(让电源键
浮在纯黑虚空中),post-boot 再 fade-in。 -->
<div id="demo-frame"
class="relative overflow-hidden bg-black"
style="
width: 360px;
height: 800px;
border-radius: 44px;
">
<!-- 开机占位(点击后被替换为 iframe) -->
<button type="button" id="demo-boot-btn"
class="ui group w-full h-full flex flex-col items-center justify-center bg-stone-950 text-stone-400 hover:text-white transition cursor-pointer focus:outline-none">
<span class="w-[72px] h-[72px] rounded-full border-2 border-stone-600 flex items-center justify-center mb-5 group-hover:border-stone-300 group-hover:scale-105 transition">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>
</span>
<span class="hero-boot-label">Click to start</span>
</button>
</div>
<!-- ============================================================ -->
<!-- State Dock — 6 个外挂图标贴在手机右边缘,点击打开右侧抽屉。 -->
<!-- 每个 .state-dock-tab 同时带 data-studio-tab,复用现有 JS。 -->
<!-- ============================================================ -->
<aside class="state-dock ui" aria-label="State Builder dock" role="toolbar">
<span class="state-dock-eyebrow" aria-hidden="true">State Builder</span>
<span class="state-dock-divider" aria-hidden="true"></span>
<button type="button" class="state-dock-tab" data-studio-tab="session" data-state-dock-tab="session" aria-selected="true" aria-label="Session snapshots">
<span class="state-dock-tab-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>
</span>
</button>
<button type="button" class="state-dock-tab" data-studio-tab="language" data-state-dock-tab="language" aria-selected="false" aria-label="Phone language">
<span class="state-dock-tab-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>
</span>
<span class="state-dock-tab-badge" id="state-dock-language-badge" aria-hidden="true"></span>
</button>
<button type="button" class="state-dock-tab" data-studio-tab="device" data-state-dock-tab="device" aria-selected="false" aria-label="Device controls">
<span class="state-dock-tab-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="2.5" width="10" height="19" rx="2.6"/><path d="M10.5 18.5h3"/></svg>
</span>
</button>
<button type="button" class="state-dock-tab" data-studio-tab="wechat" data-state-dock-tab="wechat" aria-selected="false" aria-label="WeChat data">
<span class="state-dock-tab-icon state-dock-tab-icon-img">
<img src="figures/icons/com.tencent.mm.png" alt="" loading="lazy" />
</span>
</button>
<button type="button" class="state-dock-tab" data-studio-tab="alipay" data-state-dock-tab="alipay" aria-selected="false" aria-label="Alipay data">
<span class="state-dock-tab-icon state-dock-tab-icon-img">
<img src="figures/icons/com.eg.android.AlipayGphone.png" alt="" loading="lazy" />
</span>
</button>
<button type="button" class="state-dock-tab" data-studio-tab="sms" data-state-dock-tab="sms" aria-selected="false" aria-label="SMS data">
<span class="state-dock-tab-icon state-dock-tab-icon-img">
<img src="figures/icons/com.android.mms.png" alt="" loading="lazy" />
</span>
</button>
<button type="button" class="state-dock-tab" data-studio-tab="railway" data-state-dock-tab="railway" aria-selected="false" aria-label="12306 data">
<span class="state-dock-tab-icon state-dock-tab-icon-img">
<img src="figures/icons/com.MobileTicket.png" alt="" loading="lazy" />
</span>
</button>
<button type="button" class="state-dock-tab" data-studio-tab="weather" data-state-dock-tab="weather" aria-selected="false" aria-label="Weather data">
<span class="state-dock-tab-icon state-dock-tab-icon-img">
<img src="figures/icons/com.miui.weather2.png" alt="" loading="lazy" />
</span>
</button>
</aside>
<span class="state-dock-hint" aria-hidden="true">Patch state</span>
<!-- ============================================================ -->
<!-- Gesture Guide — 左侧"软键盘 + 图例"二合一卡片,与右侧 State -->
<!-- Dock 对称。每个图标既是手势示意(SVG 内 .gesture-trail 用 -->
<!-- stroke-dasharray 动画反复"划出"轨迹),也是可点的硬键 -->
<!-- (data-gesture-action),点击直接调用 iframe 内的 -->
<!-- __OS__.handleBack / goHome / showRecents。绑定见 -->
<!-- scripts/boot-hero.js 的委托。 -->
<!-- ============================================================ -->
<aside class="gesture-guide ui" aria-label="Phone gesture controls">
<div class="gesture-guide-head">
<span class="gesture-guide-eyebrow">Gestures</span>
<span class="gesture-guide-meta">or click</span>
</div>
<ul class="gesture-guide-list" role="list">
<li>
<button type="button" class="gesture-guide-item" data-gesture-action="back" aria-label="Back — same as edge-swipe">
<span class="gesture-guide-icon" aria-hidden="true">
<svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<!-- phone left edge -->
<line x1="6" y1="5" x2="6" y2="27"/>
<!-- finger contact dot -->
<circle cx="6" cy="16" r="1.6" fill="currentColor" stroke="none"/>
<!-- swipe trail rightward -->
<path class="gesture-trail" d="M7.5 16 L 22 16"/>
<polyline class="gesture-trail" points="18.5 12, 22 16, 18.5 20"/>
</svg>
</span>
<span class="gesture-guide-text">
<span class="gesture-guide-label">Back</span>
<span class="gesture-guide-desc">Swipe in from left or right edge</span>
</span>
</button>
</li>
<li>
<button type="button" class="gesture-guide-item" data-gesture-action="home" aria-label="Home — same as swipe-up from bottom">
<span class="gesture-guide-icon" aria-hidden="true">
<svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<!-- home indicator bar at bottom -->
<line x1="11" y1="27" x2="21" y2="27" stroke-width="2"/>
<!-- finger contact dot -->
<circle cx="16" cy="24" r="1.6" fill="currentColor" stroke="none"/>
<!-- upward swipe trail -->
<path class="gesture-trail" d="M16 22 L 16 8"/>
<polyline class="gesture-trail" points="12 11.5, 16 8, 20 11.5"/>
</svg>
</span>
<span class="gesture-guide-text">
<span class="gesture-guide-label">Home</span>
<span class="gesture-guide-desc">Swipe up from bottom edge</span>
</span>
</button>
</li>
<li>
<button type="button" class="gesture-guide-item" data-gesture-action="recents" aria-label="Recents — same as swipe-up and hold">
<span class="gesture-guide-icon" aria-hidden="true">
<svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<!-- stacked recents preview cards -->
<rect x="8.5" y="5" width="15" height="8.5" rx="1.6"/>
<line x1="11.5" y1="3" x2="20.5" y2="3" opacity="0.55"/>
<!-- home indicator -->
<line x1="11" y1="27" x2="21" y2="27" stroke-width="2"/>
<!-- finger contact dot -->
<circle cx="16" cy="24" r="1.6" fill="currentColor" stroke="none"/>
<!-- short upward swipe + pause -->
<path class="gesture-trail" d="M16 22 L 16 17"/>
<polyline class="gesture-trail" points="13 19.5, 16 17, 19 19.5"/>
</svg>
</span>
<span class="gesture-guide-text">
<span class="gesture-guide-label">Recents</span>
<span class="gesture-guide-desc">Swipe up &amp; hold mid-screen</span>
</span>
</button>
</li>
<li>
<!-- Hint-only row: switching home-screen pages happens on the
desktop itself (workspace pager) — there's no single
__OS__ action to trigger from a button, so this is a
static <div> with the .gesture-guide-item--hint modifier
suppressing hover/active affordances. -->
<div class="gesture-guide-item gesture-guide-item--hint" role="note" aria-label="Switch home-screen pages by swiping or dragging horizontally">
<span class="gesture-guide-icon" aria-hidden="true">
<svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<!-- page indicator dots at top, middle one active -->
<circle cx="13" cy="7" r="0.9" fill="currentColor" stroke="none" opacity="0.4"/>
<circle cx="16" cy="7" r="1.2" fill="currentColor" stroke="none"/>
<circle cx="19" cy="7" r="0.9" fill="currentColor" stroke="none" opacity="0.4"/>
<!-- finger contact dot on the right -->
<circle cx="22" cy="18" r="1.6" fill="currentColor" stroke="none"/>
<!-- horizontal swipe trail leftward across the page -->
<path class="gesture-trail" d="M20.5 18 L 8 18"/>
<polyline class="gesture-trail" points="11.5 14.5, 8 18, 11.5 21.5"/>
</svg>
</span>
<span class="gesture-guide-text">
<span class="gesture-guide-label">Switch pages</span>
<span class="gesture-guide-desc">Swipe or drag horizontally on the desktop</span>
</span>
</div>
</li>
</ul>
</aside>
</div>
</div>
<!-- ============================================================ -->
<!-- State Popover — 锚定在 dock 右侧的浮层卡片,无遮罩。点击 dock -->
<!-- 任意 tab 滑出,点击卡片外区域 / ESC 关闭。复用原有 -->
<!-- .state-builder-tabs / .state-builder-panel-shell 子树。 -->
<!-- ============================================================ -->
<aside id="state-drawer" class="state-drawer" data-open="false" aria-label="State Builder" aria-hidden="true">
<div class="state-drawer-head">
<div>
<div class="state-builder-board-kicker">State Builder · Live Injection</div>
<h3 class="state-builder-board-title">Patch runtime state without restarting the device</h3>
<p class="state-builder-board-lede">Snapshot · time-travel · inject cross-app data while the simulator keeps running.</p>
</div>
<button type="button" id="state-drawer-close" class="state-drawer-close" aria-label="Close State Builder">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
</div>
<div class="state-drawer-body">
<div class="state-builder-board">
<div class="state-builder-tabs" role="tablist" aria-label="State Builder capabilities">
<button type="button" class="state-builder-tab" data-studio-tab="session" aria-selected="true" aria-controls="state-panel-session">
<span class="state-builder-tab-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>
</span>
<span class="state-builder-tab-label">Session</span>
</button>
<button type="button" class="state-builder-tab" data-studio-tab="language" aria-selected="false" aria-controls="state-panel-language">
<span class="state-builder-tab-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>
</span>
<span class="state-builder-tab-label">Language</span>
</button>
<button type="button" class="state-builder-tab" data-studio-tab="device" aria-selected="false" aria-controls="state-panel-device">
<span class="state-builder-tab-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="2.5" width="10" height="19" rx="2.6"/><path d="M10.5 18.5h3"/></svg>
</span>
<span class="state-builder-tab-label">Device</span>
</button>
<button type="button" class="state-builder-tab" data-studio-tab="wechat" aria-selected="false" aria-controls="state-panel-wechat">
<span class="state-builder-tab-icon state-builder-tab-icon-img">
<img src="figures/icons/com.tencent.mm.png" alt="" loading="lazy" />
</span>
<span class="state-builder-tab-label">WeChat</span>
</button>
<button type="button" class="state-builder-tab" data-studio-tab="alipay" aria-selected="false" aria-controls="state-panel-alipay">
<span class="state-builder-tab-icon state-builder-tab-icon-img">
<img src="figures/icons/com.eg.android.AlipayGphone.png" alt="" loading="lazy" />
</span>
<span class="state-builder-tab-label">Alipay</span>
</button>
<button type="button" class="state-builder-tab" data-studio-tab="sms" aria-selected="false" aria-controls="state-panel-sms">
<span class="state-builder-tab-icon state-builder-tab-icon-img">
<img src="figures/icons/com.android.mms.png" alt="" loading="lazy" />
</span>
<span class="state-builder-tab-label">SMS</span>
</button>
<button type="button" class="state-builder-tab" data-studio-tab="railway" aria-selected="false" aria-controls="state-panel-railway">
<span class="state-builder-tab-icon state-builder-tab-icon-img">
<img src="figures/icons/com.MobileTicket.png" alt="" loading="lazy" />
</span>
<span class="state-builder-tab-label">12306</span>
</button>
<button type="button" class="state-builder-tab" data-studio-tab="weather" aria-selected="false" aria-controls="state-panel-weather">
<span class="state-builder-tab-icon state-builder-tab-icon-img">
<img src="figures/icons/com.miui.weather2.png" alt="" loading="lazy" />
</span>
<span class="state-builder-tab-label">Weather</span>
</button>
</div>
<div class="state-builder-panel-shell">
<section id="state-panel-session" class="studio-panel space-y-3" data-studio-panel="session" data-active="true">
<div class="studio-action-rail" role="tablist" aria-label="Session state actions">
<button type="button" class="studio-action-tab" data-studio-action="session-reset" aria-selected="true">Reset</button>
<button type="button" class="studio-action-tab" data-studio-action="session-save" aria-selected="false">Save</button>
<button type="button" class="studio-action-tab" data-studio-action="session-restore" aria-selected="false">Restore</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="session-reset" data-active="true">
<div class="studio-snapshot-meta">Reset clears the simulator and reloads a fresh session. Saved snapshots stay in this browser.</div>
<button type="button" id="studio-reset-session" class="studio-action w-full">Reset simulator</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="session-save" data-active="false">
<div class="studio-field">
<label for="studio-snapshot-name">Snapshot name</label>
<input id="studio-snapshot-name" class="studio-input" value="Snapshot" />
</div>
<button type="button" id="studio-save-snapshot" class="studio-action w-full">Save snapshot</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="session-restore" data-active="false">
<div class="studio-field">
<label for="studio-snapshot-select">Snapshot</label>
<select id="studio-snapshot-select" class="studio-input"><option value="">No snapshots yet</option></select>
</div>
<button type="button" id="studio-restore-snapshot" class="studio-action w-full">Restore snapshot</button>
<button type="button" id="studio-delete-snapshot" class="studio-action w-full">Delete snapshot</button>
</div>
</section>
<section id="state-panel-language" class="studio-panel space-y-3" data-studio-panel="language" data-active="false">
<div class="studio-snapshot-meta">
Sets the simulated phone's language. Also clears per-app overrides (Alipay, Bilibili, RedBook, Map) so every app follows the new system locale.
</div>
<div class="studio-language-choices" role="radiogroup" aria-label="Phone language">
<button type="button" class="studio-language-choice" data-language-choice="zh-Hans" role="radio" aria-checked="true">
<span class="studio-language-choice-name">中文</span>
<span class="studio-language-choice-meta">Simplified Chinese</span>
</button>
<button type="button" class="studio-language-choice" data-language-choice="en" role="radio" aria-checked="false">
<span class="studio-language-choice-name">English</span>
<span class="studio-language-choice-meta">United States</span>
</button>
</div>
<button type="button" id="studio-apply-language" class="studio-action w-full">Apply language</button>
</section>
<section id="state-panel-device" class="studio-panel space-y-3" data-studio-panel="device" data-active="false">
<div class="studio-action-rail" role="tablist" aria-label="Device state actions">
<button type="button" class="studio-action-tab" data-studio-action="device-time" aria-selected="true">Time</button>
<button type="button" class="studio-action-tab" data-studio-action="device-battery" aria-selected="false">Battery</button>
<button type="button" class="studio-action-tab" data-studio-action="device-location" aria-selected="false">Location</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="device-time" data-active="true">
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-device-time-mode">Mode</label>
<select id="studio-device-time-mode" class="studio-input">
<option value="real">Real</option>
<option value="simulated">Simulated</option>
</select>
</div>
<div class="studio-field">
<label for="studio-device-time-flow">Flow</label>
<select id="studio-device-time-flow" class="studio-input"><option value="flow">Flow</option></select>
</div>
</div>
<div class="studio-field" id="studio-device-time-value-field" hidden>
<label for="studio-device-time-value">Device time</label>
<input id="studio-device-time-value" class="studio-input" type="datetime-local" />
</div>
<button type="button" id="studio-apply-device-time" class="studio-action w-full">Apply time</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="device-battery" data-active="false">
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-device-battery-level">Battery</label>
<input id="studio-device-battery-level" class="studio-input" type="number" min="0" max="100" step="1" value="80" />
</div>
<div class="studio-field">
<label for="studio-device-battery-status">Status</label>
<select id="studio-device-battery-status" class="studio-input">
<option value="unplugged">Unplugged</option>
<option value="charging">Charging</option>
<option value="fast">Fast charge</option>
</select>
</div>
</div>
<div class="studio-field">
<label for="studio-device-battery-saver">Battery saver</label>
<select id="studio-device-battery-saver" class="studio-input">
<option value="off">Off</option>
<option value="on">On</option>
</select>
</div>
<button type="button" id="studio-apply-device-battery" class="studio-action w-full">Apply battery</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="device-location" data-active="false">
<div class="studio-field">
<label for="studio-device-location-preset">Preset</label>
<select id="studio-device-location-preset" class="studio-input">
<option value="beijing">Beijing</option>
<option value="shanghai">Shanghai</option>
<option value="guangzhou">Guangzhou</option>
<option value="shenzhen">Shenzhen</option>
<option value="hangzhou">Hangzhou</option>
<option value="chengdu">Chengdu</option>
<option value="wuhan">Wuhan</option>
<option value="nanjing">Nanjing</option>
<option value="newyork">New York</option>
<option value="london">London</option>
<option value="tokyo">Tokyo</option>
<option value="custom">Custom coordinates</option>
</select>
</div>
<div class="grid grid-cols-2 gap-3" id="studio-device-location-custom" hidden>
<div class="studio-field">
<label for="studio-device-location-lat">Latitude</label>
<input id="studio-device-location-lat" class="studio-input" type="number" step="0.000001" value="39.9042" />
</div>
<div class="studio-field">
<label for="studio-device-location-lon">Longitude</label>
<input id="studio-device-location-lon" class="studio-input" type="number" step="0.000001" value="116.4074" />
</div>
</div>
<button type="button" id="studio-apply-device-location" class="studio-action w-full">Apply location</button>
</div>
</section>
<section id="state-panel-wechat" class="studio-panel space-y-3" data-studio-panel="wechat" data-active="false">
<div class="studio-action-rail" role="tablist" aria-label="WeChat data actions">
<button type="button" class="studio-action-tab" data-studio-action="wechat-message" aria-selected="true">Message</button>
<button type="button" class="studio-action-tab" data-studio-action="wechat-contact" aria-selected="false">Contact</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="wechat-message" data-active="true">
<div class="studio-field">
<label for="studio-wechat-contact">Chat</label>
<select id="studio-wechat-contact" class="studio-input"><option value="">陈静</option></select>
</div>
<div class="studio-field">
<label for="studio-wechat-time">Message time</label>
<input id="studio-wechat-time" class="studio-input" type="datetime-local" />
</div>
<div class="studio-field">
<label for="studio-wechat-message">Incoming message</label>
<textarea id="studio-wechat-message" class="studio-input min-h-[82px] resize-none">今晚 7 点开会,记得带材料。</textarea>
</div>
<button type="button" id="studio-insert-wechat-message" class="studio-action w-full">Insert message</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="wechat-contact" data-active="false">
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-wechat-new-name">New contact</label>
<input id="studio-wechat-new-name" class="studio-input" value="林小雨" />
</div>
<div class="studio-field">
<label for="studio-wechat-new-wxid">WeChat ID</label>
<input id="studio-wechat-new-wxid" class="studio-input" value="linxiaoyu_2026" />
</div>
</div>
<div class="studio-field">
<label>Avatar</label>
<div class="avatar-picker" id="studio-wechat-avatar-picker" aria-label="Choose WeChat avatar">
<button type="button" class="avatar-choice" data-avatar="avatars/avatar_default.jpg" aria-pressed="true" title="Default"><img src="/@app-assets/Wechat/avatars/avatar_default.jpg" alt="" /></button>
<button type="button" class="avatar-choice" data-avatar="avatars/avatar_88.jpg" aria-pressed="false" title="Avatar 88"><img src="/@app-assets/Wechat/avatars/avatar_88.jpg" alt="" /></button>
<button type="button" class="avatar-choice" data-avatar="avatars/avatar_64.jpg" aria-pressed="false" title="Avatar 64"><img src="/@app-assets/Wechat/avatars/avatar_64.jpg" alt="" /></button>
<button type="button" class="avatar-choice" data-avatar="avatars/avatar_10.jpg" aria-pressed="false" title="Avatar 10"><img src="/@app-assets/Wechat/avatars/avatar_10.jpg" alt="" /></button>
<button type="button" class="avatar-choice" data-avatar="avatars/avatar_20.jpg" aria-pressed="false" title="Avatar 20"><img src="/@app-assets/Wechat/avatars/avatar_20.jpg" alt="" /></button>
<button type="button" class="avatar-choice" data-avatar="avatars/avatar_50.jpg" aria-pressed="false" title="Avatar 50"><img src="/@app-assets/Wechat/avatars/avatar_50.jpg" alt="" /></button>
</div>
</div>
<div class="studio-field">
<label for="studio-wechat-new-signature">Signature</label>
<input id="studio-wechat-new-signature" class="studio-input" value="明天会更好" />
</div>
<button type="button" id="studio-add-wechat-contact" class="studio-action w-full">Add contact</button>
</div>
</section>
<section id="state-panel-alipay" class="studio-panel space-y-3" data-studio-panel="alipay" data-active="false">
<div class="studio-action-rail" role="tablist" aria-label="Alipay data actions">
<button type="button" class="studio-action-tab" data-studio-action="alipay-balance" aria-selected="true">Balance</button>
<button type="button" class="studio-action-tab" data-studio-action="alipay-bill" aria-selected="false">Bill</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="alipay-balance" data-active="true">
<div class="studio-field">
<label for="studio-alipay-balance">Account balance</label>
<input id="studio-alipay-balance" class="studio-input" type="number" step="0.01" value="520.13" />
</div>
<button type="button" id="studio-set-alipay-balance" class="studio-action w-full">Set balance</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="alipay-bill" data-active="false">
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-alipay-amount">Bill amount</label>
<input id="studio-alipay-amount" class="studio-input" type="number" step="0.01" value="32" />
</div>
<div class="studio-field">
<label for="studio-alipay-direction">Type</label>
<select id="studio-alipay-direction" class="studio-input">
<option value="expense">Expense</option>
<option value="income">Income</option>
</select>
</div>
</div>
<div class="studio-field">
<label for="studio-alipay-title">Bill title</label>
<input id="studio-alipay-title" class="studio-input" value="星巴克" />
</div>
<div class="studio-field">
<label for="studio-alipay-note">Note</label>
<input id="studio-alipay-note" class="studio-input" value="拿铁" />
</div>
<button type="button" id="studio-add-alipay-bill" class="studio-action w-full">Add bill</button>
</div>
</section>
<section id="state-panel-sms" class="studio-panel space-y-3" data-studio-panel="sms" data-active="false">
<div class="studio-action-rail" role="tablist" aria-label="SMS and contacts data actions">
<button type="button" class="studio-action-tab" data-studio-action="sms-contact" aria-selected="true">Contact</button>
<button type="button" class="studio-action-tab" data-studio-action="sms-message" aria-selected="false">SMS</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="sms-contact" data-active="true">
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-contact-name">Contact name</label>
<input id="studio-contact-name" class="studio-input" value="张三" />
</div>
<div class="studio-field">
<label for="studio-contact-phone">Phone</label>
<input id="studio-contact-phone" class="studio-input" value="13800138000" />
</div>
</div>
<button type="button" id="studio-save-contact" class="studio-action w-full">Save contact</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="sms-message" data-active="false">
<div class="studio-field">
<label for="studio-sms-sender">SMS sender</label>
<input id="studio-sms-sender" class="studio-input" value="12306" />
</div>
<div class="studio-field">
<label for="studio-sms-body">Message content</label>
<textarea id="studio-sms-body" class="studio-input min-h-[88px] resize-none">您的验证码是 482913,用于 MobileGym 场景演示,请勿泄露。</textarea>
</div>
<button type="button" id="studio-send-sms" class="studio-action w-full">Receive SMS</button>
</div>
</section>
<section id="state-panel-railway" class="studio-panel space-y-3" data-studio-panel="railway" data-active="false">
<div class="studio-action-rail" role="tablist" aria-label="12306 data actions">
<button type="button" class="studio-action-tab" data-studio-action="railway-order" aria-selected="true">Ticket order</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="railway-order" data-active="true">
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-railway-train">Train</label>
<input id="studio-railway-train" class="studio-input" value="G123" />
</div>
<div class="studio-field">
<label for="studio-railway-status">Status</label>
<select id="studio-railway-status" class="studio-input">
<option value="completed">Paid</option>
<option value="pending">Unpaid</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-railway-from">From station</label>
<input id="studio-railway-from" class="studio-input" value="北京南" />
</div>
<div class="studio-field">
<label for="studio-railway-to">To station</label>
<input id="studio-railway-to" class="studio-input" value="上海虹桥" />
</div>
</div>
<div class="studio-field">
<label for="studio-railway-date">Date</label>
<input id="studio-railway-date" class="studio-input" type="date" value="2026-05-03" />
</div>
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-railway-depart">Depart</label>
<input id="studio-railway-depart" class="studio-input" value="09:30" />
</div>
<div class="studio-field">
<label for="studio-railway-arrive">Arrive</label>
<input id="studio-railway-arrive" class="studio-input" value="14:48" />
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-railway-passenger">Passenger</label>
<input id="studio-railway-passenger" class="studio-input" value="林小雨" />
</div>
<div class="studio-field">
<label for="studio-railway-price">Price</label>
<input id="studio-railway-price" class="studio-input" type="number" step="0.5" value="553" />
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="studio-field">
<label for="studio-railway-seat-type">Seat type</label>
<select id="studio-railway-seat-type" class="studio-input">
<option value="商务座">商务座</option>
<option value="一等座">一等座</option>
<option value="二等座">二等座</option>
<option value="高级软卧">高级软卧</option>
<option value="软卧">软卧</option>
<option value="硬卧">硬卧</option>
<option value="软座">软座</option>
<option value="硬座">硬座</option>
<option value="无座">无座</option>
</select>
</div>
<div class="studio-field">
<label for="studio-railway-seat-no">Seat</label>
<input id="studio-railway-seat-no" class="studio-input" value="03车 08A号" />
</div>
</div>
<button type="button" id="studio-add-railway-order" class="studio-action w-full">Add ticket/order</button>
</div>
</section>
<section id="state-panel-weather" class="studio-panel space-y-3" data-studio-panel="weather" data-active="false">
<div class="studio-action-rail" role="tablist" aria-label="Weather data actions">
<button type="button" class="studio-action-tab" data-studio-action="weather-now" aria-selected="true">Conditions</button>
</div>
<div class="studio-action-panel space-y-3" data-studio-action-panel="weather-now" data-active="true">
<div class="studio-snapshot-meta">
Patches the located page's current weather. Switch city via the Device → Location panel.
</div>
<div class="studio-field">
<label for="studio-weather-condition">Condition</label>
<select id="studio-weather-condition" class="studio-input">
<option value="sunny">晴 (Sunny)</option>
<option value="cloudy">多云 (Partly cloudy)</option>
<option value="overcast">阴 (Overcast)</option>
<option value="light-rain">小雨 (Light rain)</option>
<option value="heavy-rain">大雨 (Heavy rain)</option>
<option value="thunderstorm">雷阵雨 (Thunderstorm)</option>
<option value="light-snow">小雪 (Light snow)</option>
<option value="fog">雾 (Fog)</option>
<option value="haze">霾 (Haze)</option>
</select>
</div>
<div class="grid grid-cols-3 gap-3">
<div class="studio-field">
<label for="studio-weather-temp">Temp °C</label>
<input id="studio-weather-temp" class="studio-input" type="number" step="1" value="20" />
</div>
<div class="studio-field">
<label for="studio-weather-high">High °C</label>
<input id="studio-weather-high" class="studio-input" type="number" step="1" value="25" />
</div>
<div class="studio-field">
<label for="studio-weather-low">Low °C</label>
<input id="studio-weather-low" class="studio-input" type="number" step="1" value="15" />
</div>
</div>
<div class="studio-field">
<label for="studio-weather-aqi">AQI (0500)</label>
<input id="studio-weather-aqi" class="studio-input" type="number" step="1" value="50" />
</div>
<button type="button" id="studio-apply-weather-now" class="studio-action w-full">Apply weather</button>
</div>
</section>
</div>
<div id="studio-status" class="studio-status" data-kind="warn" role="status" aria-live="polite">
Power on the phone, then apply a scenario.
</div>
<p class="state-builder-board-foot">
One <code>setState</code> patch per action · persists across reloads
</p>
</div>
</div>
</aside>
</div>
</section>
</body>
</html>