HongzeFu commited on
Commit
6d95d0c
·
1 Parent(s): a365309
gradio-web/config.py CHANGED
@@ -19,7 +19,7 @@ RIGHT_TOP_LOG_SCALE = 1
19
  UI_GLOBAL_FONT_SIZE = "24px"
20
 
21
  # Session / queue 配置
22
- SESSION_TIMEOUT = 5 # 30秒无用户主动操作后,交由 gr.State TTL 自动回收 session
23
  SESSION_CONCURRENCY_ID = "session_slots"
24
  SESSION_CONCURRENCY_LIMIT = 2
25
 
@@ -84,6 +84,10 @@ UI_TEXT = {
84
  "actions": {
85
  "point_required_suffix": " 🎯",
86
  },
 
 
 
 
87
  "errors": {
88
  "load_missing_task": "Error loading task: missing current_task",
89
  "load_invalid_task": "Error loading task: invalid task payload",
@@ -108,12 +112,12 @@ UI_ACTION_TEXT_OVERRIDES = {
108
  "move backward-left": "move backward-left↗︎",
109
  "move backward-right": "move backward-right↖︎",
110
  },
111
- # "RouteStick": {
112
- # "move to the nearest left target by circling around the stick clockwise": "move left clockwise↘︎→↗︎ ◟→◞",
113
- # "move to the nearest right target by circling around the stick clockwise": "move right clockwise↖︎←↙︎ ◟←◞",
114
- # "move to the nearest left target by circling around the stick counterclockwise": "move left counterclockwise↗︎→↘︎ ◜→◝",
115
- # "move to the nearest right target by circling around the stick counterclockwise": "move right counterclockwise↙︎←↖︎ ◜←◝",
116
- # },
117
  }
118
 
119
  ROUTESTICK_OVERLAY_ACTION_TEXTS = [
 
19
  UI_GLOBAL_FONT_SIZE = "24px"
20
 
21
  # Session / queue 配置
22
+ SESSION_TIMEOUT = 39999 # 30秒无用户主动操作后,交由 gr.State TTL 自动回收 session
23
  SESSION_CONCURRENCY_ID = "session_slots"
24
  SESSION_CONCURRENCY_LIMIT = 2
25
 
 
84
  "actions": {
85
  "point_required_suffix": " 🎯",
86
  },
87
+ "progress": {
88
+ "episode_loading": "The episode is loading...",
89
+ "queue_wait": "Lots of people are playing! Please wait...",
90
+ },
91
  "errors": {
92
  "load_missing_task": "Error loading task: missing current_task",
93
  "load_invalid_task": "Error loading task: invalid task payload",
 
112
  "move backward-left": "move backward-left↗︎",
113
  "move backward-right": "move backward-right↖︎",
114
  },
115
+ "RouteStick": {
116
+ "move to the nearest left target by circling around the stick clockwise": "move left clockwise↘︎→↗︎ ◟→◞",
117
+ "move to the nearest right target by circling around the stick clockwise": "move right clockwise↖︎←↙︎ ◟←◞",
118
+ "move to the nearest left target by circling around the stick counterclockwise": "move left counterclockwise↗︎→↘︎ ◜→◝",
119
+ "move to the nearest right target by circling around the stick counterclockwise": "move right counterclockwise↙︎←↖︎ ◜←◝",
120
+ },
121
  }
122
 
123
  ROUTESTICK_OVERLAY_ACTION_TEXTS = [
gradio-web/gradio_callbacks.py CHANGED
@@ -264,29 +264,6 @@ def show_task_hint(uid, current_hint=""):
264
  return get_task_hint(env_id)
265
 
266
 
267
- def show_loading_info():
268
- """
269
- 显示加载环境的全屏遮罩层提示信息
270
-
271
- 功能说明:
272
- - 此函数在用户点击登录/加载任务等按钮时被调用
273
- - 返回包含全屏遮罩层的 HTML 字符串,用于显示加载提示
274
- - 遮罩层会覆盖整个页面,防止用户在加载过程中进行其他操作
275
- - 加载完成后,回调函数会返回空字符串 "" 来清空 loading_overlay 组件,从而隐藏遮罩层
276
-
277
- 工作流程:
278
- 1. 用户点击按钮(如 Login、Next Task 等)
279
- 2. 按钮的 click 事件首先调用此函数,显示遮罩层
280
- 3. 然后通过 .then() 链式调用实际的加载函数(如 login_and_load_task)
281
- 4. 加载函数执行完成后,返回 gr.update(visible=False) 隐藏遮罩层
282
-
283
- Returns:
284
- gr.update: 显示 loading overlay group
285
- """
286
- LOGGER.debug("show_loading_info: displaying loading overlay")
287
- return gr.update(visible=True)
288
-
289
-
290
  def on_video_end(uid):
291
  """
292
  Called when the demonstration video finishes playing.
@@ -519,7 +496,6 @@ def _task_load_failed_response(uid, message):
519
  gr.update(visible=False), # action_phase_group
520
  gr.update(visible=False), # control_panel_group
521
  gr.update(value=""), # task_hint_display
522
- gr.update(visible=False), # loading_overlay
523
  gr.update(interactive=False), # reference_action_btn
524
  )
525
 
@@ -596,7 +572,6 @@ def _load_status_task(uid, status):
596
  gr.update(visible=True), # action_phase_group
597
  gr.update(visible=True), # control_panel_group
598
  gr.update(value=get_task_hint(env_id) if env_id else ""), # task_hint_display
599
- gr.update(visible=False), # loading_overlay
600
  gr.update(interactive=False), # reference_action_btn
601
  )
602
 
@@ -676,7 +651,6 @@ def _load_status_task(uid, status):
676
  gr.update(visible=False), # action_phase_group
677
  gr.update(visible=False), # control_panel_group
678
  gr.update(value=get_task_hint(actual_env_id)), # task_hint_display
679
- gr.update(visible=False), # loading_overlay
680
  gr.update(interactive=True), # reference_action_btn
681
  )
682
 
@@ -701,7 +675,6 @@ def _load_status_task(uid, status):
701
  gr.update(visible=True), # action_phase_group
702
  gr.update(visible=True), # control_panel_group
703
  gr.update(value=get_task_hint(actual_env_id)), # task_hint_display
704
- gr.update(visible=False), # loading_overlay
705
  gr.update(interactive=True), # reference_action_btn
706
  )
707
 
 
264
  return get_task_hint(env_id)
265
 
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  def on_video_end(uid):
268
  """
269
  Called when the demonstration video finishes playing.
 
496
  gr.update(visible=False), # action_phase_group
497
  gr.update(visible=False), # control_panel_group
498
  gr.update(value=""), # task_hint_display
 
499
  gr.update(interactive=False), # reference_action_btn
500
  )
501
 
 
572
  gr.update(visible=True), # action_phase_group
573
  gr.update(visible=True), # control_panel_group
574
  gr.update(value=get_task_hint(env_id) if env_id else ""), # task_hint_display
 
575
  gr.update(interactive=False), # reference_action_btn
576
  )
577
 
 
651
  gr.update(visible=False), # action_phase_group
652
  gr.update(visible=False), # control_panel_group
653
  gr.update(value=get_task_hint(actual_env_id)), # task_hint_display
 
654
  gr.update(interactive=True), # reference_action_btn
655
  )
656
 
 
675
  gr.update(visible=True), # action_phase_group
676
  gr.update(visible=True), # control_panel_group
677
  gr.update(value=get_task_hint(actual_env_id)), # task_hint_display
 
678
  gr.update(interactive=True), # reference_action_btn
679
  )
680
 
gradio-web/state_manager.py CHANGED
@@ -15,6 +15,7 @@ LOGGER = logging.getLogger("robomme.state_manager")
15
 
16
  # --- 全局会话存储 ---
17
  GLOBAL_SESSIONS = {}
 
18
 
19
  # --- 任务索引存储(用于进度显示) ---
20
  TASK_INDEX_MAP = {} # {uid: {"task_index": int, "total_tasks": int}}
@@ -32,6 +33,7 @@ TASK_START_TIMES = {} # {"{uid}:{env_id}:{episode_idx}": iso_timestamp}
32
  PLAY_BUTTON_CLICKED = {} # {uid: bool}
33
 
34
  _state_lock = threading.Lock()
 
35
 
36
 
37
  def get_session(uid):
@@ -40,6 +42,52 @@ def get_session(uid):
40
  return GLOBAL_SESSIONS.get(uid)
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def create_session(uid):
44
  """
45
  为指定 session key 创建 ProcessSessionProxy。
@@ -49,11 +97,16 @@ def create_session(uid):
49
  if not uid:
50
  raise ValueError("Session uid cannot be empty")
51
 
52
- with _state_lock:
53
- session = GLOBAL_SESSIONS.get(uid)
54
- if session is None:
55
- session = ProcessSessionProxy()
56
- GLOBAL_SESSIONS[uid] = session
 
 
 
 
 
57
  LOGGER.info("create_session uid=%s total_sessions=%s", uid, len(GLOBAL_SESSIONS))
58
  return uid
59
 
@@ -186,6 +239,7 @@ def cleanup_session(uid):
186
  LOGGER.info("cleanup_session uid=%s proxy closed", uid)
187
  except Exception as exc:
188
  LOGGER.exception("cleanup_session uid=%s proxy close failed: %s", uid, exc)
 
189
 
190
  from user_manager import user_manager
191
 
 
15
 
16
  # --- 全局会话存储 ---
17
  GLOBAL_SESSIONS = {}
18
+ ACTIVE_SESSION_SLOTS = set()
19
 
20
  # --- 任务索引存储(用于进度显示) ---
21
  TASK_INDEX_MAP = {} # {uid: {"task_index": int, "total_tasks": int}}
 
33
  PLAY_BUTTON_CLICKED = {} # {uid: bool}
34
 
35
  _state_lock = threading.Lock()
36
+ _session_slot_condition = threading.Condition(_state_lock)
37
 
38
 
39
  def get_session(uid):
 
42
  return GLOBAL_SESSIONS.get(uid)
43
 
44
 
45
+ def reserve_session_slot(uid):
46
+ """
47
+ Block until a session slot is available for this uid.
48
+
49
+ Slots are held for the full lifetime of a ProcessSessionProxy and released
50
+ only after cleanup closes the worker process.
51
+ """
52
+ if not uid:
53
+ raise ValueError("Session uid cannot be empty")
54
+
55
+ from config import SESSION_CONCURRENCY_LIMIT
56
+
57
+ with _session_slot_condition:
58
+ if uid in ACTIVE_SESSION_SLOTS:
59
+ return
60
+ while len(ACTIVE_SESSION_SLOTS) >= int(SESSION_CONCURRENCY_LIMIT):
61
+ LOGGER.info(
62
+ "reserve_session_slot waiting uid=%s active_slots=%s limit=%s",
63
+ uid,
64
+ len(ACTIVE_SESSION_SLOTS),
65
+ SESSION_CONCURRENCY_LIMIT,
66
+ )
67
+ _session_slot_condition.wait(timeout=0.1)
68
+ ACTIVE_SESSION_SLOTS.add(uid)
69
+ LOGGER.info(
70
+ "reserve_session_slot acquired uid=%s active_slots=%s",
71
+ uid,
72
+ len(ACTIVE_SESSION_SLOTS),
73
+ )
74
+
75
+
76
+ def release_session_slot(uid):
77
+ if not uid:
78
+ return
79
+
80
+ with _session_slot_condition:
81
+ if uid in ACTIVE_SESSION_SLOTS:
82
+ ACTIVE_SESSION_SLOTS.remove(uid)
83
+ LOGGER.info(
84
+ "release_session_slot uid=%s active_slots=%s",
85
+ uid,
86
+ len(ACTIVE_SESSION_SLOTS),
87
+ )
88
+ _session_slot_condition.notify_all()
89
+
90
+
91
  def create_session(uid):
92
  """
93
  为指定 session key 创建 ProcessSessionProxy。
 
97
  if not uid:
98
  raise ValueError("Session uid cannot be empty")
99
 
100
+ reserve_session_slot(uid)
101
+ try:
102
+ with _state_lock:
103
+ session = GLOBAL_SESSIONS.get(uid)
104
+ if session is None:
105
+ session = ProcessSessionProxy()
106
+ GLOBAL_SESSIONS[uid] = session
107
+ except Exception:
108
+ release_session_slot(uid)
109
+ raise
110
  LOGGER.info("create_session uid=%s total_sessions=%s", uid, len(GLOBAL_SESSIONS))
111
  return uid
112
 
 
239
  LOGGER.info("cleanup_session uid=%s proxy closed", uid)
240
  except Exception as exc:
241
  LOGGER.exception("cleanup_session uid=%s proxy close failed: %s", uid, exc)
242
+ release_session_slot(uid)
243
 
244
  from user_manager import user_manager
245
 
gradio-web/test/test_queue_session_limit_e2e.py CHANGED
@@ -72,11 +72,40 @@ def _minimal_load_result(uid: str, log_text: str = "ready"):
72
  gr.update(visible=True),
73
  gr.update(visible=True),
74
  gr.update(value="hint"),
75
- gr.update(visible=False),
76
  gr.update(interactive=True),
77
  )
78
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  def _mount_demo(demo):
81
  port = _free_port()
82
  host = "127.0.0.1"
@@ -93,7 +122,8 @@ def _mount_demo(demo):
93
  return root_url, demo, server, thread
94
 
95
 
96
- def test_gradio_queue_limits_init_loads_to_four(monkeypatch):
 
97
  importlib.reload(importlib.import_module("gradio_callbacks"))
98
  ui_layout = importlib.reload(importlib.import_module("ui_layout"))
99
 
@@ -111,35 +141,42 @@ def test_gradio_queue_limits_init_loads_to_four(monkeypatch):
111
  with sync_playwright() as p:
112
  browser = p.chromium.launch(headless=True)
113
  pages = []
114
- for _ in range(5):
 
115
  page = browser.new_page(viewport={"width": 1280, "height": 900})
116
  page.goto(root_url, wait_until="domcontentloaded")
117
  pages.append(page)
118
  time.sleep(0.25)
119
 
120
- snapshots = {}
121
-
122
  def _queue_snapshot_ready():
123
- texts = [page.evaluate("() => document.body.innerText") for page in pages]
124
- snapshots["texts"] = texts
125
- first_four_ready = all("processing" in text.lower() for text in texts[:4])
126
- fifth_queued = "queue:" in texts[4].lower()
127
- return first_four_ready and fifth_queued
 
 
 
 
 
 
128
 
129
  _wait_until(_queue_snapshot_ready, timeout_s=10.0)
130
-
131
- first_four = snapshots["texts"][:4]
132
- fifth_text = snapshots["texts"][4]
133
-
134
- assert all("processing" in text.lower() for text in first_four)
135
- assert "queue:" in fifth_text.lower()
136
-
137
- pages[0].wait_for_selector("#main_interface_root", state="visible", timeout=15000)
138
- pages[4].wait_for_selector("#main_interface_root", state="visible", timeout=25000)
139
-
140
- loaded_text = pages[4].evaluate("() => document.body.innerText")
141
- assert "queue:" not in loaded_text.lower()
142
- assert "processing" not in loaded_text.lower()
 
 
143
 
144
  browser.close()
145
  finally:
@@ -202,3 +239,207 @@ def test_gradio_state_ttl_cleans_up_idle_session(monkeypatch):
202
  server.should_exit = True
203
  thread.join(timeout=10)
204
  demo.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  gr.update(visible=True),
73
  gr.update(visible=True),
74
  gr.update(value="hint"),
 
75
  gr.update(interactive=True),
76
  )
77
 
78
 
79
+ def _read_progress_text(page) -> str | None:
80
+ return page.evaluate(
81
+ """() => {
82
+ const node = document.querySelector('.progress-text');
83
+ if (!node) return null;
84
+ const text = (node.textContent || '').trim();
85
+ return text || null;
86
+ }"""
87
+ )
88
+
89
+
90
+ def _read_progress_overlay_snapshot(page) -> dict[str, float | bool | None]:
91
+ return page.evaluate(
92
+ """() => {
93
+ const node = document.querySelector('#native_progress_host .wrap');
94
+ if (!node) {
95
+ return { present: false, width: null, height: null, background: null };
96
+ }
97
+ const rect = node.getBoundingClientRect();
98
+ const style = getComputedStyle(node);
99
+ return {
100
+ present: true,
101
+ width: rect.width,
102
+ height: rect.height,
103
+ background: style.backgroundColor || null,
104
+ };
105
+ }"""
106
+ )
107
+
108
+
109
  def _mount_demo(demo):
110
  port = _free_port()
111
  host = "127.0.0.1"
 
122
  return root_url, demo, server, thread
123
 
124
 
125
+ def test_gradio_queue_respects_configured_limit_on_init_load(monkeypatch):
126
+ config = importlib.reload(importlib.import_module("config"))
127
  importlib.reload(importlib.import_module("gradio_callbacks"))
128
  ui_layout = importlib.reload(importlib.import_module("ui_layout"))
129
 
 
141
  with sync_playwright() as p:
142
  browser = p.chromium.launch(headless=True)
143
  pages = []
144
+ total_pages = int(config.SESSION_CONCURRENCY_LIMIT) + 1
145
+ for _ in range(total_pages):
146
  page = browser.new_page(viewport={"width": 1280, "height": 900})
147
  page.goto(root_url, wait_until="domcontentloaded")
148
  pages.append(page)
149
  time.sleep(0.25)
150
 
 
 
151
  def _queue_snapshot_ready():
152
+ progress_texts = [_read_progress_text(page) for page in pages]
153
+ first_four_ready = all(
154
+ text and config.UI_TEXT["progress"]["episode_loading"] in text
155
+ for text in progress_texts[: config.SESSION_CONCURRENCY_LIMIT]
156
+ )
157
+ queued_text = progress_texts[-1] or ""
158
+ queued_ready = (
159
+ config.UI_TEXT["progress"]["queue_wait"] in queued_text
160
+ and "queue:" in queued_text.lower()
161
+ )
162
+ return first_four_ready and queued_ready
163
 
164
  _wait_until(_queue_snapshot_ready, timeout_s=10.0)
165
+ active_pages = [_read_progress_text(page) or "" for page in pages[: config.SESSION_CONCURRENCY_LIMIT]]
166
+ queued_text = _read_progress_text(pages[-1]) or ""
167
+
168
+ assert all(config.UI_TEXT["progress"]["episode_loading"] in text for text in active_pages)
169
+ assert config.UI_TEXT["progress"]["queue_wait"] in queued_text
170
+ assert "queue:" in queued_text.lower()
171
+ assert pages[0].evaluate("() => !!document.getElementById('loading_overlay_group')") is False
172
+ overlay_snapshot = _read_progress_overlay_snapshot(pages[0])
173
+ assert overlay_snapshot["present"] is True
174
+ assert overlay_snapshot["width"] and overlay_snapshot["width"] > 0
175
+ assert overlay_snapshot["height"] and overlay_snapshot["height"] >= 400
176
+ assert overlay_snapshot["background"] == "rgba(255, 255, 255, 0.92)"
177
+
178
+ _wait_until(lambda: _read_progress_text(pages[0]) is None, timeout_s=15.0)
179
+ _wait_until(lambda: _read_progress_text(pages[-1]) is None, timeout_s=25.0)
180
 
181
  browser.close()
182
  finally:
 
239
  server.should_exit = True
240
  thread.join(timeout=10)
241
  demo.close()
242
+
243
+
244
+ def test_single_load_uses_native_episode_loading_copy(monkeypatch):
245
+ config = importlib.reload(importlib.import_module("config"))
246
+ importlib.reload(importlib.import_module("gradio_callbacks"))
247
+ ui_layout = importlib.reload(importlib.import_module("ui_layout"))
248
+
249
+ def fake_init_app(request):
250
+ uid = str(getattr(request, "session_hash", "missing"))
251
+ time.sleep(2.5)
252
+ return _minimal_load_result(uid)
253
+
254
+ monkeypatch.setattr(ui_layout, "init_app", fake_init_app)
255
+
256
+ demo = ui_layout.create_ui_blocks()
257
+ root_url, demo, server, thread = _mount_demo(demo)
258
+
259
+ try:
260
+ with sync_playwright() as p:
261
+ browser = p.chromium.launch(headless=True)
262
+ page = browser.new_page(viewport={"width": 1280, "height": 900})
263
+ page.goto(root_url, wait_until="domcontentloaded")
264
+
265
+ _wait_until(
266
+ lambda: (_read_progress_text(page) or "").startswith(config.UI_TEXT["progress"]["episode_loading"]),
267
+ timeout_s=8.0,
268
+ )
269
+ assert page.evaluate("() => !!document.getElementById('loading_overlay_group')") is False
270
+ overlay_snapshot = _read_progress_overlay_snapshot(page)
271
+ assert overlay_snapshot["present"] is True
272
+ assert overlay_snapshot["width"] and overlay_snapshot["width"] > 0
273
+ assert overlay_snapshot["height"] and overlay_snapshot["height"] >= 400
274
+ assert overlay_snapshot["background"] == "rgba(255, 255, 255, 0.92)"
275
+
276
+ page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
277
+ _wait_until(lambda: _read_progress_text(page) is None, timeout_s=8.0)
278
+
279
+ browser.close()
280
+ finally:
281
+ server.should_exit = True
282
+ thread.join(timeout=10)
283
+ demo.close()
284
+
285
+
286
+ def test_execute_does_not_use_episode_loading_copy(monkeypatch):
287
+ importlib.reload(importlib.import_module("gradio_callbacks"))
288
+ ui_layout = importlib.reload(importlib.import_module("ui_layout"))
289
+
290
+ obs = Image.new("RGB", (32, 32), color=(10, 20, 30))
291
+
292
+ def fake_init_app(request):
293
+ uid = str(getattr(request, "session_hash", "missing"))
294
+ return _minimal_load_result(uid, log_text="ready")
295
+
296
+ def fake_precheck_execute_inputs(uid, option_idx, coords_str):
297
+ return None
298
+
299
+ def fake_switch_to_execute_phase(uid):
300
+ return (
301
+ gr.update(interactive=False),
302
+ gr.update(interactive=False),
303
+ gr.update(interactive=False),
304
+ gr.update(interactive=False),
305
+ gr.update(interactive=False),
306
+ gr.update(interactive=False),
307
+ )
308
+
309
+ def fake_execute_step(uid, option_idx, coords_str):
310
+ time.sleep(1.5)
311
+ return (
312
+ gr.update(value=obs, interactive=False),
313
+ "executed",
314
+ "BinFill (Episode 1)",
315
+ "Completed: 0",
316
+ gr.update(interactive=True),
317
+ gr.update(interactive=True),
318
+ gr.update(interactive=True),
319
+ )
320
+
321
+ def fake_switch_to_action_phase(uid=None):
322
+ return (
323
+ gr.update(interactive=True),
324
+ gr.update(),
325
+ gr.update(),
326
+ gr.update(),
327
+ gr.update(interactive=True),
328
+ gr.update(interactive=True),
329
+ )
330
+
331
+ monkeypatch.setattr(ui_layout, "init_app", fake_init_app)
332
+ monkeypatch.setattr(ui_layout, "precheck_execute_inputs", fake_precheck_execute_inputs)
333
+ monkeypatch.setattr(ui_layout, "switch_to_execute_phase", fake_switch_to_execute_phase)
334
+ monkeypatch.setattr(ui_layout, "execute_step", fake_execute_step)
335
+ monkeypatch.setattr(ui_layout, "switch_to_action_phase", fake_switch_to_action_phase)
336
+
337
+ demo = ui_layout.create_ui_blocks()
338
+ root_url, demo, server, thread = _mount_demo(demo)
339
+
340
+ try:
341
+ with sync_playwright() as p:
342
+ browser = p.chromium.launch(headless=True)
343
+ page = browser.new_page(viewport={"width": 1280, "height": 900})
344
+ page.goto(root_url, wait_until="domcontentloaded")
345
+ page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
346
+ page.locator("#exec_btn button, button#exec_btn").first.click()
347
+ page.wait_for_timeout(500)
348
+
349
+ body_text = page.evaluate("() => document.body.innerText")
350
+ assert "The episode is loading..." not in body_text
351
+ assert "Lots of people are playing! Please wait..." not in body_text
352
+
353
+ browser.close()
354
+ finally:
355
+ server.should_exit = True
356
+ thread.join(timeout=10)
357
+ demo.close()
358
+
359
+
360
+ def test_late_user_waits_for_active_session_slot_release(monkeypatch):
361
+ config = importlib.reload(importlib.import_module("config"))
362
+ state_manager = importlib.reload(importlib.import_module("state_manager"))
363
+ callbacks = importlib.reload(importlib.import_module("gradio_callbacks"))
364
+ ui_layout = importlib.reload(importlib.import_module("ui_layout"))
365
+
366
+ closed = []
367
+
368
+ class _FakeProxy:
369
+ def __init__(self):
370
+ self.env_id = None
371
+ self.episode_idx = None
372
+ self.language_goal = "goal"
373
+ self.available_options = [("pick", 0)]
374
+ self.raw_solve_options = [{"label": "a", "action": "pick", "available": False}]
375
+ self.demonstration_frames = []
376
+
377
+ def load_episode(self, env_id, episode_idx):
378
+ self.env_id = env_id
379
+ self.episode_idx = episode_idx
380
+ return Image.new("RGB", (32, 32), color=(10, 20, 30)), "loaded"
381
+
382
+ def get_pil_image(self, use_segmented=False):
383
+ _ = use_segmented
384
+ return Image.new("RGB", (32, 32), color=(10, 20, 30))
385
+
386
+ def close(self):
387
+ closed.append((self.env_id, self.episode_idx))
388
+
389
+ monkeypatch.setattr(state_manager, "ProcessSessionProxy", _FakeProxy)
390
+ monkeypatch.setattr(
391
+ callbacks.user_manager,
392
+ "init_session",
393
+ lambda uid: (
394
+ True,
395
+ "ok",
396
+ {"current_task": {"env_id": "BinFill", "episode_idx": 1}, "completed_count": 0},
397
+ ),
398
+ )
399
+ monkeypatch.setattr(callbacks, "get_task_hint", lambda env_id: "")
400
+ monkeypatch.setattr(callbacks, "should_show_demo_video", lambda env_id: False)
401
+
402
+ demo = ui_layout.create_ui_blocks()
403
+ root_url, demo, server, thread = _mount_demo(demo)
404
+
405
+ try:
406
+ with sync_playwright() as p:
407
+ browser = p.chromium.launch(headless=True)
408
+
409
+ page1 = browser.new_page(viewport={"width": 1280, "height": 900})
410
+ page1.goto(root_url, wait_until="domcontentloaded")
411
+ _wait_until(lambda: len(state_manager.GLOBAL_SESSIONS) == 1, timeout_s=15.0)
412
+ _wait_until(lambda: _read_progress_text(page1) is None, timeout_s=15.0)
413
+
414
+ page2 = browser.new_page(viewport={"width": 1280, "height": 900})
415
+ page2.goto(root_url, wait_until="domcontentloaded")
416
+ _wait_until(lambda: len(state_manager.GLOBAL_SESSIONS) == 2, timeout_s=15.0)
417
+ _wait_until(lambda: _read_progress_text(page2) is None, timeout_s=15.0)
418
+
419
+ assert len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT
420
+ assert len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT
421
+
422
+ page3 = browser.new_page(viewport={"width": 1280, "height": 900})
423
+ page3.goto(root_url, wait_until="domcontentloaded")
424
+
425
+ _wait_until(
426
+ lambda: (_read_progress_text(page3) or "").startswith(config.UI_TEXT["progress"]["episode_loading"]),
427
+ timeout_s=10.0,
428
+ )
429
+ time.sleep(1.0)
430
+ assert len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT
431
+ assert len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT
432
+ assert _read_progress_text(page3) is not None
433
+
434
+ page1.close()
435
+
436
+ _wait_until(lambda: len(closed) >= 1, timeout_s=10.0)
437
+ _wait_until(lambda: _read_progress_text(page3) is None, timeout_s=15.0)
438
+ _wait_until(lambda: len(state_manager.GLOBAL_SESSIONS) == config.SESSION_CONCURRENCY_LIMIT, timeout_s=10.0)
439
+ _wait_until(lambda: len(state_manager.ACTIVE_SESSION_SLOTS) == config.SESSION_CONCURRENCY_LIMIT, timeout_s=10.0)
440
+
441
+ browser.close()
442
+ finally:
443
+ server.should_exit = True
444
+ thread.join(timeout=10)
445
+ demo.close()
gradio-web/test/test_ui_native_layout_contract.py CHANGED
@@ -36,8 +36,7 @@ def test_native_ui_css_uses_configured_global_font_size_variables(reload_module)
36
  assert f"--button-large-text-size: {config.UI_GLOBAL_FONT_SIZE} !important;" in css
37
  assert f"--section-header-text-size: {config.UI_GLOBAL_FONT_SIZE} !important;" in css
38
  assert f"--text-md: {config.UI_GLOBAL_FONT_SIZE} !important;" in css
39
- assert "#loading_overlay_group h3" in css
40
- assert f"font-size: {config.UI_GLOBAL_FONT_SIZE} !important;" in css
41
 
42
 
43
  def test_native_ui_css_excludes_header_title_from_global_font_size(reload_module):
@@ -53,8 +52,7 @@ def test_native_ui_forces_light_theme_and_uses_light_overlay_baseline(reload_mod
53
  css = ui_layout.CSS
54
 
55
  assert "color-scheme: light !important;" in css
56
- assert "background: rgba(255, 255, 255, 0.92) !important;" in css
57
- assert "color: var(--body-text-color) !important;" in css
58
  assert "body.dark," not in css
59
  assert ".dark," not in css
60
  assert ":root.dark" not in css
@@ -137,7 +135,6 @@ def test_native_ui_config_contains_phase_machine_and_precheck_chain(reload_modul
137
 
138
  required_ids = {
139
  "header_task",
140
- "loading_overlay_group",
141
  "main_layout_row",
142
  "media_card",
143
  "log_card",
@@ -166,15 +163,13 @@ def test_native_ui_config_contains_phase_machine_and_precheck_chain(reload_modul
166
  if "value" in comp.get("props", {})
167
  ]
168
  assert all("_anchor" not in str(v) for v in values)
169
- assert any(
170
- "The episode is loading..." in str(v)
171
- for v in values
172
- )
173
  assert all(
174
  "Logging in and setting up environment... Please wait." not in str(v)
175
  for v in values
176
  )
177
  assert all("Loading environment, please wait..." not in str(v) for v in values)
 
 
178
 
179
  log_output_comp = next(
180
  comp
 
36
  assert f"--button-large-text-size: {config.UI_GLOBAL_FONT_SIZE} !important;" in css
37
  assert f"--section-header-text-size: {config.UI_GLOBAL_FONT_SIZE} !important;" in css
38
  assert f"--text-md: {config.UI_GLOBAL_FONT_SIZE} !important;" in css
39
+ assert "#load_status_mode" not in css
 
40
 
41
 
42
  def test_native_ui_css_excludes_header_title_from_global_font_size(reload_module):
 
52
  css = ui_layout.CSS
53
 
54
  assert "color-scheme: light !important;" in css
55
+ assert "#loading_overlay_group" not in css
 
56
  assert "body.dark," not in css
57
  assert ".dark," not in css
58
  assert ":root.dark" not in css
 
135
 
136
  required_ids = {
137
  "header_task",
 
138
  "main_layout_row",
139
  "media_card",
140
  "log_card",
 
163
  if "value" in comp.get("props", {})
164
  ]
165
  assert all("_anchor" not in str(v) for v in values)
 
 
 
 
166
  assert all(
167
  "Logging in and setting up environment... Please wait." not in str(v)
168
  for v in values
169
  )
170
  assert all("Loading environment, please wait..." not in str(v) for v in values)
171
+ assert "The episode is loading..." in ui_layout.PROGRESS_TEXT_REWRITE_JS
172
+ assert "Lots of people are playing! Please wait..." in ui_layout.PROGRESS_TEXT_REWRITE_JS
173
 
174
  log_output_comp = next(
175
  comp
gradio-web/test/test_ui_phase_machine_runtime_e2e.py CHANGED
@@ -1043,9 +1043,6 @@ def test_unified_loading_overlay_init_flow(monkeypatch):
1043
  fake_obs_img = Image.fromarray(fake_obs)
1044
  calls = {"init": 0}
1045
 
1046
- def fake_show_loading_info():
1047
- return gr.update(visible=True)
1048
-
1049
  def fake_init_app(_request=None):
1050
  calls["init"] += 1
1051
  time.sleep(0.8)
@@ -1068,11 +1065,9 @@ def test_unified_loading_overlay_init_flow(monkeypatch):
1068
  gr.update(visible=True), # action_phase_group
1069
  gr.update(visible=True), # control_panel_group
1070
  gr.update(value="hint"), # task_hint_display
1071
- gr.update(visible=False), # loading_overlay
1072
  gr.update(interactive=True), # reference_action_btn
1073
  )
1074
 
1075
- monkeypatch.setattr(ui_layout, "show_loading_info", fake_show_loading_info)
1076
  monkeypatch.setattr(ui_layout, "init_app", fake_init_app)
1077
 
1078
  demo = ui_layout.create_ui_blocks()
@@ -1098,26 +1093,29 @@ def test_unified_loading_overlay_init_flow(monkeypatch):
1098
  page = browser.new_page(viewport={"width": 1280, "height": 900})
1099
  page.goto(root_url, wait_until="domcontentloaded")
1100
 
1101
- page.wait_for_selector("#loading_overlay_group", state="visible", timeout=2500)
1102
-
1103
- overlay_text = page.evaluate(
1104
  """() => {
1105
- const el = document.getElementById('loading_overlay_group');
1106
- return el ? (el.textContent || '') : '';
1107
  }"""
 
 
1108
  )
1109
- page.wait_for_function(
1110
  """() => {
1111
- const heading = document.querySelector('#loading_overlay_group h3');
1112
- return !!heading && getComputedStyle(heading).fontSize === '32px';
1113
- }""",
1114
- timeout=5000,
1115
  )
1116
- assert canonical_copy in overlay_text
1117
- assert superseded_copy not in overlay_text
1118
  assert legacy_copy not in page.content()
 
1119
 
1120
- page.wait_for_selector("#loading_overlay_group", state="hidden", timeout=15000)
 
 
 
1121
  page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
1122
  page.wait_for_function(
1123
  """() => {
@@ -1167,7 +1165,6 @@ def test_no_video_task_hides_manual_demo_button(monkeypatch):
1167
  gr.update(visible=True), # action_phase_group
1168
  gr.update(visible=True), # control_panel_group
1169
  gr.update(value="hint"), # task_hint_display
1170
- gr.update(visible=False), # loading_overlay
1171
  gr.update(interactive=True), # reference_action_btn
1172
  )
1173
 
@@ -1274,7 +1271,6 @@ def test_point_wait_state_pulses_live_obs_and_updates_system_log(monkeypatch):
1274
  gr.update(visible=True), # action_phase_group
1275
  gr.update(visible=True), # control_panel_group
1276
  gr.update(value="hint"), # task_hint_display
1277
- gr.update(visible=False), # loading_overlay
1278
  gr.update(interactive=True), # reference_action_btn
1279
  )
1280
 
@@ -1499,7 +1495,6 @@ def test_reference_action_single_click_applies_coords_without_wait_state(monkeyp
1499
  gr.update(visible=True), # action_phase_group
1500
  gr.update(visible=True), # control_panel_group
1501
  gr.update(value="hint"), # task_hint_display
1502
- gr.update(visible=False), # loading_overlay
1503
  gr.update(interactive=True), # reference_action_btn
1504
  )
1505
 
@@ -1651,7 +1646,6 @@ def test_live_obs_client_resize_fills_width_and_keeps_click_mapping(monkeypatch)
1651
  gr.update(visible=True), # action_phase_group
1652
  gr.update(visible=True), # control_panel_group
1653
  gr.update(value="hint"), # task_hint_display
1654
- gr.update(visible=False), # loading_overlay
1655
  gr.update(interactive=True), # reference_action_btn
1656
  )
1657
 
@@ -1879,7 +1873,6 @@ def test_header_task_shows_env_after_init(monkeypatch):
1879
  gr.update(visible=True), # action_phase_group
1880
  gr.update(visible=True), # control_panel_group
1881
  gr.update(value="hint"), # task_hint_display
1882
- gr.update(visible=False), # loading_overlay
1883
  gr.update(interactive=True), # reference_action_btn
1884
  )
1885
 
@@ -1950,7 +1943,6 @@ def test_header_goal_capitalizes_displayed_value_after_init(monkeypatch):
1950
  gr.update(visible=True), # action_phase_group
1951
  gr.update(visible=True), # control_panel_group
1952
  gr.update(value="hint"), # task_hint_display
1953
- gr.update(visible=False), # loading_overlay
1954
  gr.update(interactive=True), # reference_action_btn
1955
  )
1956
 
@@ -2026,7 +2018,6 @@ def test_header_task_env_normalization_and_fallback(monkeypatch, task_info_text,
2026
  gr.update(visible=True), # action_phase_group
2027
  gr.update(visible=True), # control_panel_group
2028
  gr.update(value="hint"), # task_hint_display
2029
- gr.update(visible=False), # loading_overlay
2030
  gr.update(interactive=True), # reference_action_btn
2031
  )
2032
 
@@ -2098,7 +2089,6 @@ def test_header_task_switch_to_video_task_shows_demo_phase(monkeypatch):
2098
  gr.update(visible=not show_video), # action_phase_group
2099
  gr.update(visible=not show_video), # control_panel_group
2100
  gr.update(value="video hint" if show_video else "hint"), # task_hint_display
2101
- gr.update(visible=False), # loading_overlay
2102
  gr.update(interactive=True), # reference_action_btn
2103
  )
2104
 
 
1043
  fake_obs_img = Image.fromarray(fake_obs)
1044
  calls = {"init": 0}
1045
 
 
 
 
1046
  def fake_init_app(_request=None):
1047
  calls["init"] += 1
1048
  time.sleep(0.8)
 
1065
  gr.update(visible=True), # action_phase_group
1066
  gr.update(visible=True), # control_panel_group
1067
  gr.update(value="hint"), # task_hint_display
 
1068
  gr.update(interactive=True), # reference_action_btn
1069
  )
1070
 
 
1071
  monkeypatch.setattr(ui_layout, "init_app", fake_init_app)
1072
 
1073
  demo = ui_layout.create_ui_blocks()
 
1093
  page = browser.new_page(viewport={"width": 1280, "height": 900})
1094
  page.goto(root_url, wait_until="domcontentloaded")
1095
 
1096
+ page.wait_for_function(
 
 
1097
  """() => {
1098
+ const node = document.querySelector('.progress-text');
1099
+ return !!node && (node.textContent || '').includes('The episode is loading...');
1100
  }"""
1101
+ ,
1102
+ timeout=5000,
1103
  )
1104
+ progress_text = page.evaluate(
1105
  """() => {
1106
+ const node = document.querySelector('.progress-text');
1107
+ return node ? (node.textContent || '') : '';
1108
+ }"""
 
1109
  )
1110
+ assert canonical_copy in progress_text
1111
+ assert superseded_copy not in progress_text
1112
  assert legacy_copy not in page.content()
1113
+ assert page.locator("#loading_overlay_group").count() == 0
1114
 
1115
+ page.wait_for_function(
1116
+ """() => !document.querySelector('.progress-text')""",
1117
+ timeout=15000,
1118
+ )
1119
  page.wait_for_selector("#main_interface_root", state="visible", timeout=15000)
1120
  page.wait_for_function(
1121
  """() => {
 
1165
  gr.update(visible=True), # action_phase_group
1166
  gr.update(visible=True), # control_panel_group
1167
  gr.update(value="hint"), # task_hint_display
 
1168
  gr.update(interactive=True), # reference_action_btn
1169
  )
1170
 
 
1271
  gr.update(visible=True), # action_phase_group
1272
  gr.update(visible=True), # control_panel_group
1273
  gr.update(value="hint"), # task_hint_display
 
1274
  gr.update(interactive=True), # reference_action_btn
1275
  )
1276
 
 
1495
  gr.update(visible=True), # action_phase_group
1496
  gr.update(visible=True), # control_panel_group
1497
  gr.update(value="hint"), # task_hint_display
 
1498
  gr.update(interactive=True), # reference_action_btn
1499
  )
1500
 
 
1646
  gr.update(visible=True), # action_phase_group
1647
  gr.update(visible=True), # control_panel_group
1648
  gr.update(value="hint"), # task_hint_display
 
1649
  gr.update(interactive=True), # reference_action_btn
1650
  )
1651
 
 
1873
  gr.update(visible=True), # action_phase_group
1874
  gr.update(visible=True), # control_panel_group
1875
  gr.update(value="hint"), # task_hint_display
 
1876
  gr.update(interactive=True), # reference_action_btn
1877
  )
1878
 
 
1943
  gr.update(visible=True), # action_phase_group
1944
  gr.update(visible=True), # control_panel_group
1945
  gr.update(value="hint"), # task_hint_display
 
1946
  gr.update(interactive=True), # reference_action_btn
1947
  )
1948
 
 
2018
  gr.update(visible=True), # action_phase_group
2019
  gr.update(visible=True), # control_panel_group
2020
  gr.update(value="hint"), # task_hint_display
 
2021
  gr.update(interactive=True), # reference_action_btn
2022
  )
2023
 
 
2089
  gr.update(visible=not show_video), # action_phase_group
2090
  gr.update(visible=not show_video), # control_panel_group
2091
  gr.update(value="video hint" if show_video else "hint"), # task_hint_display
 
2092
  gr.update(interactive=True), # reference_action_btn
2093
  )
2094
 
gradio-web/ui_layout.py CHANGED
@@ -5,6 +5,7 @@ Two-column layout: Point Selection | Right Panel.
5
  """
6
 
7
  import ast
 
8
 
9
  import gradio as gr
10
 
@@ -19,6 +20,7 @@ from config import (
19
  POINT_SELECTION_SCALE,
20
  RIGHT_TOP_ACTION_SCALE,
21
  RIGHT_TOP_LOG_SCALE,
 
22
  UI_GLOBAL_FONT_SIZE,
23
  get_live_obs_elem_classes,
24
  )
@@ -36,7 +38,6 @@ from gradio_callbacks import (
36
  precheck_execute_inputs,
37
  refresh_live_obs,
38
  restart_episode_wrapper,
39
- show_loading_info,
40
  switch_env_wrapper,
41
  switch_to_action_phase,
42
  switch_to_execute_phase,
@@ -49,6 +50,8 @@ PHASE_INIT = "init"
49
  PHASE_DEMO_VIDEO = "demo_video"
50
  PHASE_ACTION_POINT = "action_point"
51
  PHASE_EXECUTION_PLAYBACK = "execution_playback"
 
 
52
 
53
  APP_THEME = gr.themes.Default()
54
 
@@ -333,6 +336,159 @@ THEME_LOCK_JS = r"""
333
  """
334
 
335
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  CSS = f"""
337
  :root {{
338
  --body-text-size: {UI_GLOBAL_FONT_SIZE} !important;
@@ -357,27 +513,28 @@ CSS = f"""
357
  font-size: var(--text-xxl) !important;
358
  }}
359
 
360
- #loading_overlay_group {{
361
  position: fixed !important;
362
  inset: 0 !important;
363
  z-index: 9999 !important;
364
- background: rgba(255, 255, 255, 0.92) !important;
365
- color: var(--body-text-color) !important;
366
- text-align: center !important;
367
  }}
368
 
369
- #loading_overlay_group > div {{
370
- min-height: 100%;
371
- display: flex;
372
- align-items: center;
373
- justify-content: center;
374
  }}
375
 
376
- #loading_overlay_group h3 {{
377
- margin: 0 !important;
378
- font-size: {UI_GLOBAL_FONT_SIZE} !important;
 
 
 
 
379
  }}
380
 
 
381
  #reference_action_btn button:not(:disabled),
382
  button#reference_action_btn:not(:disabled) {{
383
  background: #1f8b4c !important;
@@ -524,7 +681,7 @@ def _with_phase_from_load(load_result):
524
 
525
 
526
  def _skip_load_flow():
527
- return tuple(gr.skip() for _ in range(21))
528
 
529
 
530
  def _phase_visibility_updates(phase):
@@ -587,12 +744,6 @@ def create_ui_blocks():
587
  elem_id="header_goal",
588
  )
589
 
590
- loading_overlay = gr.Markdown(
591
- "### The episode is loading...",
592
- visible=True,
593
- elem_id="loading_overlay_group",
594
- )
595
-
596
  uid_state = gr.State(
597
  value=None,
598
  time_to_live=SESSION_TIMEOUT,
@@ -607,7 +758,13 @@ def create_ui_blocks():
607
  progress_info_box = gr.Textbox(visible=False)
608
  goal_box = gr.Textbox(visible=False)
609
 
610
- with gr.Column(visible=False, elem_id="main_interface_root") as main_interface:
 
 
 
 
 
 
611
  with gr.Row(elem_id="main_layout_row"):
612
  with gr.Column(scale=POINT_SELECTION_SCALE):
613
  with gr.Column(elem_classes=["native-card"], elem_id="media_card"):
@@ -741,7 +898,6 @@ def create_ui_blocks():
741
  action_phase_group,
742
  control_panel_group,
743
  task_hint_display,
744
- loading_overlay,
745
  reference_action_btn,
746
  ui_phase_state,
747
  ]
@@ -810,17 +966,6 @@ def create_ui_blocks():
810
  normalized_current_env = _normalize_env_choice(current_task_env, base_choices)
811
  return normalized_selected_env, normalized_current_env
812
 
813
- def prepare_header_task_switch(selected_env, current_task_env):
814
- normalized_selected_env, normalized_current_env = _normalize_selected_env(
815
- selected_env,
816
- current_task_env,
817
- )
818
- if not normalized_selected_env:
819
- return gr.update(visible=False)
820
- if normalized_selected_env == normalized_current_env:
821
- return gr.update(visible=False)
822
- return gr.update(visible=False)
823
-
824
  def maybe_switch_env_with_phase(uid, selected_env, current_task_env):
825
  normalized_selected_env, normalized_current_env = _normalize_selected_env(
826
  selected_env,
@@ -845,18 +990,15 @@ def create_ui_blocks():
845
  show_progress="hidden",
846
  )
847
 
848
- header_task_box.select(
849
- fn=prepare_header_task_switch,
850
- inputs=[header_task_box, current_task_env_state],
851
- outputs=[loading_overlay],
852
- queue=False,
853
- show_progress="hidden",
854
- ).then(
855
  fn=maybe_switch_env_with_phase,
856
  inputs=[uid_state, header_task_box, current_task_env_state],
857
  outputs=load_flow_outputs,
858
  concurrency_id=SESSION_CONCURRENCY_ID,
859
  concurrency_limit=SESSION_CONCURRENCY_LIMIT,
 
 
 
860
  ).then(
861
  fn=_phase_visibility_updates,
862
  inputs=[ui_phase_state],
@@ -876,13 +1018,28 @@ def create_ui_blocks():
876
  queue=False,
877
  show_progress="hidden",
878
  )
 
 
 
 
 
 
 
 
 
 
 
 
879
 
880
- next_task_btn.click(
881
  fn=load_next_task_with_phase,
882
  inputs=[uid_state],
883
  outputs=load_flow_outputs,
884
  concurrency_id=SESSION_CONCURRENCY_ID,
885
  concurrency_limit=SESSION_CONCURRENCY_LIMIT,
 
 
 
886
  ).then(
887
  fn=_phase_visibility_updates,
888
  inputs=[ui_phase_state],
@@ -902,13 +1059,28 @@ def create_ui_blocks():
902
  queue=False,
903
  show_progress="hidden",
904
  )
 
 
 
 
 
 
 
 
 
 
 
 
905
 
906
- restart_episode_btn.click(
907
  fn=restart_episode_with_phase,
908
  inputs=[uid_state],
909
  outputs=load_flow_outputs,
910
  concurrency_id=SESSION_CONCURRENCY_ID,
911
  concurrency_limit=SESSION_CONCURRENCY_LIMIT,
 
 
 
912
  ).then(
913
  fn=_phase_visibility_updates,
914
  inputs=[ui_phase_state],
@@ -928,6 +1100,18 @@ def create_ui_blocks():
928
  queue=False,
929
  show_progress="hidden",
930
  )
 
 
 
 
 
 
 
 
 
 
 
 
931
 
932
  video_display.end(
933
  fn=on_video_end_transition,
@@ -1124,11 +1308,20 @@ def create_ui_blocks():
1124
  )
1125
 
1126
  demo.load(
 
 
 
 
 
 
1127
  fn=init_app_with_phase,
1128
  inputs=[],
1129
  outputs=load_flow_outputs,
1130
  concurrency_id=SESSION_CONCURRENCY_ID,
1131
  concurrency_limit=SESSION_CONCURRENCY_LIMIT,
 
 
 
1132
  ).then(
1133
  fn=_phase_visibility_updates,
1134
  inputs=[ui_phase_state],
@@ -1148,6 +1341,18 @@ def create_ui_blocks():
1148
  queue=False,
1149
  show_progress="hidden",
1150
  )
 
 
 
 
 
 
 
 
 
 
 
 
1151
 
1152
  demo.unload(fn=cleanup_current_request_session)
1153
  demo.queue(max_size=None, default_concurrency_limit=None)
 
5
  """
6
 
7
  import ast
8
+ import json
9
 
10
  import gradio as gr
11
 
 
20
  POINT_SELECTION_SCALE,
21
  RIGHT_TOP_ACTION_SCALE,
22
  RIGHT_TOP_LOG_SCALE,
23
+ UI_TEXT,
24
  UI_GLOBAL_FONT_SIZE,
25
  get_live_obs_elem_classes,
26
  )
 
38
  precheck_execute_inputs,
39
  refresh_live_obs,
40
  restart_episode_wrapper,
 
41
  switch_env_wrapper,
42
  switch_to_action_phase,
43
  switch_to_execute_phase,
 
50
  PHASE_DEMO_VIDEO = "demo_video"
51
  PHASE_ACTION_POINT = "action_point"
52
  PHASE_EXECUTION_PLAYBACK = "execution_playback"
53
+ LOAD_STATUS_MODE_IDLE = "idle"
54
+ LOAD_STATUS_MODE_EPISODE_LOAD = "episode_load"
55
 
56
  APP_THEME = gr.themes.Default()
57
 
 
336
  """
337
 
338
 
339
+ SET_EPISODE_LOAD_MODE_JS = f"""
340
+ () => {{
341
+ window.__robommeLoadStatusMode = {json.dumps(LOAD_STATUS_MODE_EPISODE_LOAD)};
342
+ }}
343
+ """
344
+
345
+
346
+ SET_EPISODE_LOAD_MODE_IF_SWITCH_JS = f"""
347
+ (_uid, selectedEnv, currentTaskEnv) => {{
348
+ const normalize = (value) => (value == null ? "" : String(value).trim().toLowerCase());
349
+ const nextEnv = normalize(selectedEnv);
350
+ const currentEnv = normalize(currentTaskEnv);
351
+ window.__robommeLoadStatusMode =
352
+ nextEnv && nextEnv !== currentEnv
353
+ ? {json.dumps(LOAD_STATUS_MODE_EPISODE_LOAD)}
354
+ : {json.dumps(LOAD_STATUS_MODE_IDLE)};
355
+ }}
356
+ """
357
+
358
+
359
+ RESET_EPISODE_LOAD_MODE_JS = f"""
360
+ () => {{
361
+ window.__robommeLoadStatusMode = {json.dumps(LOAD_STATUS_MODE_IDLE)};
362
+ }}
363
+ """
364
+
365
+
366
+ PROGRESS_TEXT_REWRITE_JS = f"""
367
+ () => {{
368
+ const modeEpisodeLoad = {json.dumps(LOAD_STATUS_MODE_EPISODE_LOAD)};
369
+ const modeIdle = {json.dumps(LOAD_STATUS_MODE_IDLE)};
370
+ const episodeLoadingText = {json.dumps(UI_TEXT["progress"]["episode_loading"])};
371
+ const queueWaitText = {json.dumps(UI_TEXT["progress"]["queue_wait"])};
372
+
373
+ window.__robommeLoadStatusMode = window.__robommeLoadStatusMode || modeIdle;
374
+ const getMode = () => window.__robommeLoadStatusMode || modeIdle;
375
+
376
+ const ensureOverlayStyles = () => {{
377
+ const host = document.getElementById("native_progress_host");
378
+ if (!(host instanceof HTMLElement)) {{
379
+ return;
380
+ }}
381
+ host.style.setProperty("position", "fixed", "important");
382
+ host.style.setProperty("inset", "0", "important");
383
+ host.style.setProperty("z-index", "9999", "important");
384
+ host.style.setProperty("pointer-events", "none", "important");
385
+ host.style.setProperty("width", "100vw", "important");
386
+ host.style.setProperty("height", "100vh", "important");
387
+ host.style.setProperty("min-height", "100vh", "important");
388
+ host.style.setProperty("overflow", "visible", "important");
389
+
390
+ const wrap = host.querySelector(".wrap");
391
+ if (wrap instanceof HTMLElement) {{
392
+ wrap.style.setProperty("position", "fixed", "important");
393
+ wrap.style.setProperty("inset", "0", "important");
394
+ wrap.style.setProperty("width", "100vw", "important");
395
+ wrap.style.setProperty("height", "100vh", "important");
396
+ wrap.style.setProperty("min-height", "100vh", "important");
397
+ wrap.style.setProperty("padding", "0", "important");
398
+ wrap.style.setProperty("display", "flex", "important");
399
+ wrap.style.setProperty("align-items", "center", "important");
400
+ wrap.style.setProperty("justify-content", "center", "important");
401
+ wrap.style.setProperty("background", "rgba(255, 255, 255, 0.92)", "important");
402
+ wrap.style.setProperty("backdrop-filter", "blur(2px)", "important");
403
+ }}
404
+ }};
405
+
406
+ const splitSegments = (text) =>
407
+ text
408
+ .split("|")
409
+ .map((part) => part.trim())
410
+ .filter(Boolean);
411
+
412
+ const rewriteNode = (node) => {{
413
+ if (!(node instanceof HTMLElement)) {{
414
+ return;
415
+ }}
416
+
417
+ const displayed = (node.innerText || node.textContent || "").trim();
418
+ const previousCustom = node.dataset.robommeProgressCustom || "";
419
+ const raw =
420
+ node.dataset.robommeProgressCustomized === "1" && displayed === previousCustom
421
+ ? node.dataset.robommeProgressRaw || displayed
422
+ : displayed;
423
+
424
+ if (getMode() !== modeEpisodeLoad) {{
425
+ if (
426
+ node.dataset.robommeProgressCustomized === "1" &&
427
+ displayed === previousCustom &&
428
+ node.dataset.robommeProgressRaw
429
+ ) {{
430
+ node.textContent = node.dataset.robommeProgressRaw;
431
+ }}
432
+ delete node.dataset.robommeProgressCustomized;
433
+ delete node.dataset.robommeProgressRaw;
434
+ delete node.dataset.robommeProgressCustom;
435
+ return;
436
+ }}
437
+
438
+ const normalized = raw.toLowerCase();
439
+ let custom = null;
440
+
441
+ if (normalized.startsWith("processing")) {{
442
+ const segments = splitSegments(raw);
443
+ const suffix = segments.length > 1 ? ` | ${{segments.slice(1).join(" | ")}}` : "";
444
+ custom = `${{episodeLoadingText}}${{suffix}}`;
445
+ }} else if (normalized.startsWith("queue:")) {{
446
+ custom = `${{queueWaitText}} | ${{raw}}`;
447
+ }}
448
+
449
+ if (!custom) {{
450
+ return;
451
+ }}
452
+
453
+ node.dataset.robommeProgressCustomized = "1";
454
+ node.dataset.robommeProgressRaw = raw;
455
+ node.dataset.robommeProgressCustom = custom;
456
+ if (displayed !== custom) {{
457
+ node.textContent = custom;
458
+ }}
459
+ }};
460
+
461
+ const rewriteAll = () => {{
462
+ ensureOverlayStyles();
463
+ document.querySelectorAll(".progress-text").forEach(rewriteNode);
464
+ }};
465
+
466
+ const scheduleRewrite = () => {{
467
+ if (window.__robommeProgressRewriteRaf) {{
468
+ return;
469
+ }}
470
+ window.__robommeProgressRewriteRaf = window.requestAnimationFrame(() => {{
471
+ window.__robommeProgressRewriteRaf = null;
472
+ rewriteAll();
473
+ }});
474
+ }};
475
+
476
+ if (!window.__robommeProgressRewriteInstalled) {{
477
+ const observer = new MutationObserver(scheduleRewrite);
478
+ observer.observe(document.body, {{
479
+ childList: true,
480
+ subtree: true,
481
+ characterData: true,
482
+ }});
483
+ window.setInterval(scheduleRewrite, 200);
484
+ window.__robommeProgressRewriteInstalled = true;
485
+ }}
486
+
487
+ scheduleRewrite();
488
+ }}
489
+ """
490
+
491
+
492
  CSS = f"""
493
  :root {{
494
  --body-text-size: {UI_GLOBAL_FONT_SIZE} !important;
 
513
  font-size: var(--text-xxl) !important;
514
  }}
515
 
516
+ #native_progress_host {{
517
  position: fixed !important;
518
  inset: 0 !important;
519
  z-index: 9999 !important;
520
+ pointer-events: none !important;
 
 
521
  }}
522
 
523
+ #native_progress_host .wrap {{
524
+ width: 100vw !important;
525
+ min-height: 100vh !important;
 
 
526
  }}
527
 
528
+ #native_progress_host .wrap.translucent {{
529
+ background: rgba(255, 255, 255, 0.92) !important;
530
+ backdrop-filter: blur(2px);
531
+ }}
532
+
533
+ #native_progress_host .pending {{
534
+ min-height: 100vh !important;
535
  }}
536
 
537
+
538
  #reference_action_btn button:not(:disabled),
539
  button#reference_action_btn:not(:disabled) {{
540
  background: #1f8b4c !important;
 
681
 
682
 
683
  def _skip_load_flow():
684
+ return tuple(gr.skip() for _ in range(20))
685
 
686
 
687
  def _phase_visibility_updates(phase):
 
744
  elem_id="header_goal",
745
  )
746
 
 
 
 
 
 
 
747
  uid_state = gr.State(
748
  value=None,
749
  time_to_live=SESSION_TIMEOUT,
 
758
  progress_info_box = gr.Textbox(visible=False)
759
  goal_box = gr.Textbox(visible=False)
760
 
761
+ with gr.Column(visible=True, elem_id="main_interface_root") as main_interface:
762
+ native_progress_host = gr.Markdown(
763
+ value="",
764
+ visible=True,
765
+ container=False,
766
+ elem_id="native_progress_host",
767
+ )
768
  with gr.Row(elem_id="main_layout_row"):
769
  with gr.Column(scale=POINT_SELECTION_SCALE):
770
  with gr.Column(elem_classes=["native-card"], elem_id="media_card"):
 
898
  action_phase_group,
899
  control_panel_group,
900
  task_hint_display,
 
901
  reference_action_btn,
902
  ui_phase_state,
903
  ]
 
966
  normalized_current_env = _normalize_env_choice(current_task_env, base_choices)
967
  return normalized_selected_env, normalized_current_env
968
 
 
 
 
 
 
 
 
 
 
 
 
969
  def maybe_switch_env_with_phase(uid, selected_env, current_task_env):
970
  normalized_selected_env, normalized_current_env = _normalize_selected_env(
971
  selected_env,
 
990
  show_progress="hidden",
991
  )
992
 
993
+ header_task_switch = header_task_box.select(
 
 
 
 
 
 
994
  fn=maybe_switch_env_with_phase,
995
  inputs=[uid_state, header_task_box, current_task_env_state],
996
  outputs=load_flow_outputs,
997
  concurrency_id=SESSION_CONCURRENCY_ID,
998
  concurrency_limit=SESSION_CONCURRENCY_LIMIT,
999
+ show_progress="full",
1000
+ js=SET_EPISODE_LOAD_MODE_IF_SWITCH_JS,
1001
+ show_progress_on=[native_progress_host],
1002
  ).then(
1003
  fn=_phase_visibility_updates,
1004
  inputs=[ui_phase_state],
 
1018
  queue=False,
1019
  show_progress="hidden",
1020
  )
1021
+ header_task_switch.success(
1022
+ fn=None,
1023
+ js=RESET_EPISODE_LOAD_MODE_JS,
1024
+ queue=False,
1025
+ show_progress="hidden",
1026
+ )
1027
+ header_task_switch.failure(
1028
+ fn=None,
1029
+ js=RESET_EPISODE_LOAD_MODE_JS,
1030
+ queue=False,
1031
+ show_progress="hidden",
1032
+ )
1033
 
1034
+ next_task_click = next_task_btn.click(
1035
  fn=load_next_task_with_phase,
1036
  inputs=[uid_state],
1037
  outputs=load_flow_outputs,
1038
  concurrency_id=SESSION_CONCURRENCY_ID,
1039
  concurrency_limit=SESSION_CONCURRENCY_LIMIT,
1040
+ show_progress="full",
1041
+ js=SET_EPISODE_LOAD_MODE_JS,
1042
+ show_progress_on=[native_progress_host],
1043
  ).then(
1044
  fn=_phase_visibility_updates,
1045
  inputs=[ui_phase_state],
 
1059
  queue=False,
1060
  show_progress="hidden",
1061
  )
1062
+ next_task_click.success(
1063
+ fn=None,
1064
+ js=RESET_EPISODE_LOAD_MODE_JS,
1065
+ queue=False,
1066
+ show_progress="hidden",
1067
+ )
1068
+ next_task_click.failure(
1069
+ fn=None,
1070
+ js=RESET_EPISODE_LOAD_MODE_JS,
1071
+ queue=False,
1072
+ show_progress="hidden",
1073
+ )
1074
 
1075
+ restart_episode_click = restart_episode_btn.click(
1076
  fn=restart_episode_with_phase,
1077
  inputs=[uid_state],
1078
  outputs=load_flow_outputs,
1079
  concurrency_id=SESSION_CONCURRENCY_ID,
1080
  concurrency_limit=SESSION_CONCURRENCY_LIMIT,
1081
+ show_progress="full",
1082
+ js=SET_EPISODE_LOAD_MODE_JS,
1083
+ show_progress_on=[native_progress_host],
1084
  ).then(
1085
  fn=_phase_visibility_updates,
1086
  inputs=[ui_phase_state],
 
1100
  queue=False,
1101
  show_progress="hidden",
1102
  )
1103
+ restart_episode_click.success(
1104
+ fn=None,
1105
+ js=RESET_EPISODE_LOAD_MODE_JS,
1106
+ queue=False,
1107
+ show_progress="hidden",
1108
+ )
1109
+ restart_episode_click.failure(
1110
+ fn=None,
1111
+ js=RESET_EPISODE_LOAD_MODE_JS,
1112
+ queue=False,
1113
+ show_progress="hidden",
1114
+ )
1115
 
1116
  video_display.end(
1117
  fn=on_video_end_transition,
 
1308
  )
1309
 
1310
  demo.load(
1311
+ fn=None,
1312
+ js=PROGRESS_TEXT_REWRITE_JS,
1313
+ queue=False,
1314
+ )
1315
+
1316
+ init_load = demo.load(
1317
  fn=init_app_with_phase,
1318
  inputs=[],
1319
  outputs=load_flow_outputs,
1320
  concurrency_id=SESSION_CONCURRENCY_ID,
1321
  concurrency_limit=SESSION_CONCURRENCY_LIMIT,
1322
+ show_progress="full",
1323
+ js=SET_EPISODE_LOAD_MODE_JS,
1324
+ show_progress_on=[native_progress_host],
1325
  ).then(
1326
  fn=_phase_visibility_updates,
1327
  inputs=[ui_phase_state],
 
1341
  queue=False,
1342
  show_progress="hidden",
1343
  )
1344
+ init_load.success(
1345
+ fn=None,
1346
+ js=RESET_EPISODE_LOAD_MODE_JS,
1347
+ queue=False,
1348
+ show_progress="hidden",
1349
+ )
1350
+ init_load.failure(
1351
+ fn=None,
1352
+ js=RESET_EPISODE_LOAD_MODE_JS,
1353
+ queue=False,
1354
+ show_progress="hidden",
1355
+ )
1356
 
1357
  demo.unload(fn=cleanup_current_request_session)
1358
  demo.queue(max_size=None, default_concurrency_limit=None)