@@ -164,6 +164,32 @@ def filter_agent_id(self, agent_id: list[tuple]):
164164}
165165"""
166166
167+ # Keyboard shortcut JavaScript - based on https://github.com/gradio-app/gradio/issues/6101
168+ shortcut_js = """
169+ <script>
170+ function shortcuts(e) {
171+ var event = document.all ? window.event : e;
172+ switch (e.target.tagName.toLowerCase()) {
173+ case "input":
174+ case "textarea":
175+ case "select":
176+ case "button":
177+ return;
178+ default:
179+ if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && (e.metaKey || e.ctrlKey)) {
180+ e.preventDefault();
181+ if (e.key === 'ArrowLeft') {
182+ document.getElementById("prev_btn").click();
183+ } else {
184+ document.getElementById("next_btn").click();
185+ }
186+ }
187+ }
188+ }
189+ document.addEventListener('keydown', shortcuts, false);
190+ </script>
191+ """
192+
167193
168194def run_gradio (results_dir : Path ):
169195 """
@@ -173,14 +199,12 @@ def run_gradio(results_dir: Path):
173199 global info
174200 info .results_dir = results_dir
175201
176- with gr .Blocks (theme = gr .themes .Soft (), css = css ) as demo :
202+ with gr .Blocks (theme = gr .themes .Soft (), css = css , head = shortcut_js ) as demo :
177203 agent_id = gr .State (value = None )
178204 episode_id = gr .State (value = EpisodeId ())
179205 agent_task_id = gr .State (value = None )
180206 step_id = gr .State (value = None )
181207
182- hidden_key_input = gr .Textbox (visible = False , elem_id = "key_capture" )
183-
184208 with gr .Accordion ("Help" , open = False ):
185209 gr .Markdown (
186210 """\
@@ -302,6 +326,16 @@ def run_gradio(results_dir: Path):
302326 action_info = gr .Markdown (label = "Action Info" , elem_classes = "my-markdown" )
303327 state_error = gr .Markdown (label = "Next Step Error" , elem_classes = "my-markdown" )
304328
329+ with gr .Row (variant = "panel" , elem_classes = ["items-center" , "justify-center" ]):
330+ step_indicator = gr .Markdown ("### Step 0/0" , elem_classes = ["text-center" ])
331+ prev_btn = gr .Button (
332+ "◀ Previous" , size = "md" , scale = 0 , elem_id = "prev_btn" , elem_classes = ["mx-auto" ]
333+ )
334+ next_btn = gr .Button (
335+ "Next ▶" , size = "md" , scale = 0 , elem_id = "next_btn" , elem_classes = ["mx-auto" ]
336+ )
337+ gr .Markdown ("(Shortcut: Ctrl/Cmd + ← →)" , elem_classes = ["text-center" ])
338+
305339 profiling_gr = gr .Image (
306340 label = "Profiling" , show_label = False , interactive = False , show_download_button = False
307341 )
@@ -511,31 +545,37 @@ def run_gradio(results_dir: Path):
511545
512546 demo .load (fn = refresh_exp_dir_choices , inputs = exp_dir_choice , outputs = exp_dir_choice )
513547
514- demo .load (
515- None ,
516- None ,
517- None ,
518- js = """
519- function() {
520- document.addEventListener('keydown', function(e) {
521- if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && (e.metaKey || e.ctrlKey)) {
522- e.preventDefault();
523- const hiddenInput = document.querySelector('#key_capture input, #key_capture textarea');
524- if (hiddenInput) {
525- let event = e.key === 'ArrowLeft' ? 'Cmd+Left' : 'Cmd+Right';
526- hiddenInput.value = event;
527- hiddenInput.dispatchEvent(new Event('input', {bubbles: true}));
528- }
529- }
530- });
531- }
532- """ ,
533- )
534- hidden_key_input .change (
535- handle_key_event ,
536- inputs = [hidden_key_input , step_id ],
537- outputs = [hidden_key_input , step_id ],
538- )
548+ # Simple navigation button events
549+ def navigate_prev (step_id : StepId ):
550+ global info
551+ if step_id and step_id .step is not None and step_id .episode_id :
552+ step = max (0 , step_id .step - 1 )
553+ info .step = step
554+ return StepId (episode_id = step_id .episode_id , step = step )
555+ return step_id
556+
557+ def navigate_next (step_id : StepId ):
558+ global info
559+ if step_id and step_id .step is not None and step_id .episode_id and info .exp_result :
560+ step = min (len (info .exp_result .steps_info ) - 1 , step_id .step + 1 )
561+ info .step = step
562+ return StepId (episode_id = step_id .episode_id , step = step )
563+ return step_id
564+
565+ prev_btn .click (navigate_prev , inputs = [step_id ], outputs = [step_id ])
566+ next_btn .click (navigate_next , inputs = [step_id ], outputs = [step_id ])
567+
568+ # Update step indicator display
569+ def format_step_indicator (step_id ):
570+ global info
571+ if not step_id or not info .exp_result or not info .exp_result .steps_info :
572+ return "### Step 0/0"
573+ # 1-based for user, total steps is len-1 (last is terminal)
574+ current = (step_id .step + 1 ) if step_id .step is not None else 0
575+ total = max (len (info .exp_result .steps_info ) - 1 , 0 )
576+ return f"### Step { current } /{ total } "
577+
578+ step_id .change (format_step_indicator , inputs = [step_id ], outputs = [step_indicator ])
539579
540580 demo .queue ()
541581
@@ -546,25 +586,6 @@ def run_gradio(results_dir: Path):
546586 demo .launch (server_port = port , share = do_share )
547587
548588
549- def handle_key_event (key_event , step_id : StepId ):
550-
551- if key_event :
552- global info
553-
554- # print(f"Key event: {key_event}")
555- step = step_id .step
556- if key_event .startswith ("Cmd+Left" ):
557- step = max (0 , step - 1 )
558- elif key_event .startswith ("Cmd+Right" ):
559- step = min (len (info .exp_result .steps_info ) - 2 , step + 1 )
560- else :
561- return gr .update ()
562- # print(f"Updating step to {step} from key event {key_event}")
563- info .step = step
564- step_id = StepId (episode_id = step_id .episode_id , step = step )
565- return ("" , step_id )
566-
567-
568589def tab_select (evt : gr .SelectData ):
569590 global info
570591 info .active_tab = evt .value
@@ -947,7 +968,7 @@ def get_episode_info(info: Info):
947968
948969 info = f"""\
949970 ### { env_args .task_name } (seed: { env_args .task_seed } )
950- ### Step { info . step } / { len ( steps_info ) - 1 } (Reward: { cum_reward :.1f} )
971+ ### (Reward: { cum_reward :.1f} )
951972
952973**Goal:**
953974
0 commit comments