From 66694c663d77b810a191e6b2fb5675a6b0724ced Mon Sep 17 00:00:00 2001 From: Zilong Zhou Date: Fri, 18 Jul 2025 01:58:20 +0800 Subject: [PATCH] Feat/monitor cache (#267) * feat&style: add task status configuration and clear cache functionality; enhance UI styles * feat&refactor: enhance current configuration API and improve cache clearing logic * refactor&style: simplify task status update logic and improve page refresh mechanism * refactor&feat: streamline default configuration retrieval and enhance cache initialization logic * feat&refactor: add caching to default configuration retrieval and streamline task status logic * feat&style: add collapsible section for additional model parameters and enhance styling for config items * refactor&style: remove floating action button and clean up related styles --- monitor/main.py | 328 ++++++++++++++++++++++++---------- monitor/static/index.css | 31 ---- monitor/static/index.js | 335 ++++++++++++++++++----------------- monitor/static/style.css | 43 +++++ monitor/templates/index.html | 6 +- 5 files changed, 450 insertions(+), 293 deletions(-) diff --git a/monitor/main.py b/monitor/main.py index 5b56ea1..88145bf 100644 --- a/monitor/main.py +++ b/monitor/main.py @@ -5,10 +5,8 @@ from functools import cache import os import json import time -import subprocess from datetime import datetime -from pathlib import Path -from flask import Flask, render_template_string, jsonify, send_file, request, render_template +from flask import Flask, jsonify, send_file, request, render_template from dotenv import load_dotenv @@ -36,15 +34,11 @@ else: EXAMPLES_BASE_PATH = os.getenv("EXAMPLES_BASE_PATH", "../evaluation_examples/examples") RESULTS_BASE_PATH = os.getenv("RESULTS_BASE_PATH", "../results") -ACTION_SPACE=os.getenv("ACTION_SPACE", "pyautogui") -OBSERVATION_TYPE=os.getenv("OBSERVATION_TYPE", "screenshot") -MODEL_NAME=os.getenv("MODEL_NAME", "computer-use-preview") MAX_STEPS = int(os.getenv("MAX_STEPS", "150")) -def initialize_default_config(): - """Initialize default configuration from the first available config in results directory""" - global ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME, RESULTS_PATH, MAX_STEPS - +@cache +def get_default_config(): + """Get the first available configuration from results directory""" if os.path.exists(RESULTS_BASE_PATH): try: # Scan for the first available configuration @@ -57,34 +51,38 @@ def initialize_default_config(): for model_name in os.listdir(obs_path): model_path = os.path.join(obs_path, model_name) if os.path.isdir(model_path): - # Use the first available configuration as default - ACTION_SPACE = action_space - OBSERVATION_TYPE = obs_type - MODEL_NAME = model_name - RESULTS_PATH = model_path - - # Read max_steps from args.json if available + # Get max_steps from args.json if available model_args = get_model_args(action_space, obs_type, model_name) + max_steps = MAX_STEPS if model_args and 'max_steps' in model_args: - MAX_STEPS = model_args['max_steps'] + max_steps = model_args['max_steps'] - print(f"Initialized default config: {ACTION_SPACE}/{OBSERVATION_TYPE}/{MODEL_NAME} (max_steps: {MAX_STEPS})") - return + print(f"Found default config: {action_space}/{obs_type}/{model_name} (max_steps: {max_steps})") + return { + 'action_space': action_space, + 'observation_type': obs_type, + 'model_name': model_name, + 'max_steps': max_steps + } except Exception as e: print(f"Error scanning results directory for default config: {e}") - # Fallback to original environment-based path if no configs found - RESULTS_PATH = os.path.join(RESULTS_BASE_PATH, ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME) - print(f"Using fallback config from environment: {ACTION_SPACE}/{OBSERVATION_TYPE}/{MODEL_NAME} (max_steps: {MAX_STEPS})") + # Fallback to environment-based config if no configs found + fallback_config = { + 'action_space': os.getenv("ACTION_SPACE", "pyautogui"), + 'observation_type': os.getenv("OBSERVATION_TYPE", "screenshot"), + 'model_name': os.getenv("MODEL_NAME", "computer-use-preview"), + 'max_steps': MAX_STEPS + } + print(f"Using fallback config from environment: {fallback_config['action_space']}/{fallback_config['observation_type']}/{fallback_config['model_name']} (max_steps: {fallback_config['max_steps']})") + return fallback_config -# Initialize default configuration -initialize_default_config() - -RESULTS_PATH = os.path.join(RESULTS_BASE_PATH, ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME) - -if RESULTS_PATH not in TASK_STATUS_CACHE: - # Initialize cache for this results path - TASK_STATUS_CACHE[RESULTS_PATH] = {} +def ensure_cache_initialized(action_space, observation_type, model_name): + """Ensure cache is initialized for the given configuration""" + results_path = os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name) + if results_path not in TASK_STATUS_CACHE: + TASK_STATUS_CACHE[results_path] = {} + return results_path @cache def load_task_list(): @@ -99,8 +97,16 @@ def get_task_info(task_type, task_id): return json.load(f) return None -def get_task_status(task_type, task_id): - result_dir = os.path.join(RESULTS_PATH, task_type, task_id) +def get_task_status_with_config(task_type, task_id, action_space, observation_type, model_name): + results_path = os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name) + max_steps = MAX_STEPS + + # Get max_steps from args.json if available + model_args = get_model_args(action_space, observation_type, model_name) + if model_args and 'max_steps' in model_args: + max_steps = model_args['max_steps'] + + result_dir = os.path.join(results_path, task_type, task_id) if not os.path.exists(result_dir): return { @@ -152,7 +158,7 @@ def get_task_status(task_type, task_id): log_content = f.readlines() last_response = None - for i, line in enumerate(log_content): + for line in log_content: # Extract agent responses for each step if "Responses: [" in line: response_text = line.split("Responses: [")[1].strip() @@ -192,7 +198,7 @@ def get_task_status(task_type, task_id): status = "Done (Message Exit)" elif log_data.get("exit_condition") and "thought_exit: True" in log_data.get("exit_condition", ""): status = "Done (Thought Exit)" - elif len(steps) >= MAX_STEPS: + elif len(steps) >= max_steps: status = "Done (Max Steps)" else: status = "Running" @@ -214,25 +220,41 @@ def get_task_status(task_type, task_id): return { "status": status, "progress": len(steps), - "max_steps": MAX_STEPS, + "max_steps": max_steps, "last_update": last_update, "steps": steps, "log_data": log_data, "result": result_content } -def get_task_status_brief(task_type, task_id): +def get_task_status(task_type, task_id): + # This function should not be used anymore - use get_task_status_with_config instead + default_config = get_default_config() + return get_task_status_with_config(task_type, task_id, + default_config['action_space'], + default_config['observation_type'], + default_config['model_name']) + +def get_task_status_brief_with_config(task_type, task_id, action_space, observation_type, model_name): """ Get brief status info for a task, without detailed step data, for fast homepage loading. """ - # Generate cache key based on task type and ID - cache_key = f"{task_type}_{task_id}" + results_path = os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name) + max_steps = MAX_STEPS + + # Get max_steps from args.json if available + model_args = get_model_args(action_space, observation_type, model_name) + if model_args and 'max_steps' in model_args: + max_steps = model_args['max_steps'] + + # Generate cache key based on task type, ID, and config + cache_key = f"{task_type}_{task_id}_{action_space}_{observation_type}_{model_name}" # Check if the status is already cached current_time = time.time() last_cache_time = None - if cache_key in TASK_STATUS_CACHE[RESULTS_PATH]: - cached_status, cached_time = TASK_STATUS_CACHE[RESULTS_PATH][cache_key] + if results_path in TASK_STATUS_CACHE and cache_key in TASK_STATUS_CACHE[results_path]: + cached_status, cached_time = TASK_STATUS_CACHE[results_path][cache_key] last_cache_time = cached_time # If cached status is "Done", check if it's within the stability period if cached_status["status"].startswith("Done"): @@ -247,13 +269,13 @@ def get_task_status_brief(task_type, task_id): # For non-Done status (like Error), just return from cache return cached_status - result_dir = os.path.join(RESULTS_PATH, task_type, task_id) + result_dir = os.path.join(results_path, task_type, task_id) if not os.path.exists(result_dir): return { "status": "Not Started", "progress": 0, - "max_steps": MAX_STEPS, + "max_steps": max_steps, "last_update": None } @@ -265,7 +287,7 @@ def get_task_status_brief(task_type, task_id): return { "status": "Preparing", "progress": 0, - "max_steps": MAX_STEPS, + "max_steps": max_steps, "last_update": datetime.fromtimestamp(os.path.getmtime(result_dir)).strftime("%Y-%m-%d %H:%M:%S") } @@ -296,7 +318,7 @@ def get_task_status_brief(task_type, task_id): return { "status": "Initializing", "progress": 0, - "max_steps": MAX_STEPS, + "max_steps": max_steps, "last_update": datetime.fromtimestamp(os.path.getmtime(traj_file)).strftime("%Y-%m-%d %H:%M:%S") } @@ -311,7 +333,7 @@ def get_task_status_brief(task_type, task_id): status = "Error" # If step count reaches max, consider as done - if step_count >= MAX_STEPS: + if step_count >= max_steps: status = "Done (Max Steps)" # Quickly check exit condition in log file (only last few lines) @@ -329,7 +351,7 @@ def get_task_status_brief(task_type, task_id): pass # If step count reaches max again (double check) - if step_count >= MAX_STEPS: + if step_count >= max_steps: status = "Done (Max Steps)" # Get last update time @@ -352,18 +374,34 @@ def get_task_status_brief(task_type, task_id): status_dict = { "status": status, "progress": step_count, - "max_steps": MAX_STEPS, + "max_steps": max_steps, "last_update": last_update, "result": result_content } + # Initialize cache for this results path if it doesn't exist + if results_path not in TASK_STATUS_CACHE: + TASK_STATUS_CACHE[results_path] = {} + # Cache the status if it is done or error if status.startswith("Done") or status == "Error": current_time = last_cache_time if last_cache_time else current_time - TASK_STATUS_CACHE[RESULTS_PATH][cache_key] = (status_dict, current_time) + TASK_STATUS_CACHE[results_path][cache_key] = (status_dict, current_time) return status_dict +def get_task_status_brief(task_type, task_id): + """ + Get brief status info for a task, without detailed step data, for fast homepage loading. + """ + # This function should not be used anymore - use get_task_status_brief_with_config instead + default_config = get_default_config() + return get_task_status_brief_with_config(task_type, task_id, + default_config['action_space'], + default_config['observation_type'], + default_config['model_name']) + + def get_all_tasks_status(): task_list = load_task_list() result = {} @@ -389,6 +427,59 @@ def get_all_tasks_status(): return result +def get_all_tasks_status_with_config(action_space, observation_type, model_name): + task_list = load_task_list() + result = {} + + for task_type, task_ids in task_list.items(): + result[task_type] = [] + for task_id in task_ids: + task_info = get_task_info(task_type, task_id) + task_status = get_task_status_with_config(task_type, task_id, action_space, observation_type, model_name) + + if task_info: + result[task_type].append({ + "id": task_id, + "instruction": task_info.get("instruction", "No instruction provided"), + "status": task_status + }) + else: + result[task_type].append({ + "id": task_id, + "instruction": "No task info available", + "status": task_status + }) + + return result + +def get_all_tasks_status_brief_with_config(action_space, observation_type, model_name): + """ + Get brief status info for all tasks, without detailed step data, for fast homepage loading. + """ + task_list = load_task_list() + result = {} + + for task_type, task_ids in task_list.items(): + result[task_type] = [] + for task_id in task_ids: + task_info = get_task_info(task_type, task_id) + task_status = get_task_status_brief_with_config(task_type, task_id, action_space, observation_type, model_name) + + if task_info: + result[task_type].append({ + "id": task_id, + "instruction": task_info.get("instruction", "No instruction provided"), + "status": task_status + }) + else: + result[task_type].append({ + "id": task_id, + "instruction": "No task info available", + "status": task_status + }) + + return result + def get_all_tasks_status_brief(): """ Get brief status info for all tasks, without detailed step data, for fast homepage loading. @@ -423,8 +514,14 @@ def index(): @app.route('/task//') def task_detail(task_type, task_id): + # Get config from URL parameters + default_config = get_default_config() + action_space = request.args.get('action_space', default_config['action_space']) + observation_type = request.args.get('observation_type', default_config['observation_type']) + model_name = request.args.get('model_name', default_config['model_name']) + task_info = get_task_info(task_type, task_id) - task_status = get_task_status(task_type, task_id) + task_status = get_task_status_with_config(task_type, task_id, action_space, observation_type, model_name) if not task_info: return "Task not found", 404 @@ -433,22 +530,44 @@ def task_detail(task_type, task_id): task_id=task_id, task_type=task_type, task_info=task_info, - task_status=task_status) + task_status=task_status, + action_space=action_space, + observation_type=observation_type, + model_name=model_name) @app.route('/api/tasks') def api_tasks(): """Task status API""" - return jsonify(get_all_tasks_status()) + # Get config from URL parameters + default_config = get_default_config() + action_space = request.args.get('action_space', default_config['action_space']) + observation_type = request.args.get('observation_type', default_config['observation_type']) + model_name = request.args.get('model_name', default_config['model_name']) + + return jsonify(get_all_tasks_status_with_config(action_space, observation_type, model_name)) @app.route('/api/tasks/brief') def api_tasks_brief(): """Return brief status info for all tasks, without detailed step data, for fast homepage loading.""" - return jsonify(get_all_tasks_status_brief()) + # Get config from URL parameters + default_config = get_default_config() + action_space = request.args.get('action_space', default_config['action_space']) + observation_type = request.args.get('observation_type', default_config['observation_type']) + model_name = request.args.get('model_name', default_config['model_name']) + + return jsonify(get_all_tasks_status_brief_with_config(action_space, observation_type, model_name)) @app.route('/task///screenshot/') def task_screenshot(task_type, task_id, filename): """Get task screenshot""" - screenshot_path = os.path.join(RESULTS_PATH, task_type, task_id, filename) + # Get config from URL parameters + default_config = get_default_config() + action_space = request.args.get('action_space', default_config['action_space']) + observation_type = request.args.get('observation_type', default_config['observation_type']) + model_name = request.args.get('model_name', default_config['model_name']) + + results_path = os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name) + screenshot_path = os.path.join(results_path, task_type, task_id, filename) if os.path.exists(screenshot_path): return send_file(screenshot_path, mimetype='image/png') else: @@ -457,7 +576,14 @@ def task_screenshot(task_type, task_id, filename): @app.route('/task///recording') def task_recording(task_type, task_id): """Get task recording video""" - recording_path = os.path.join(RESULTS_PATH, task_type, task_id, "recording.mp4") + # Get config from URL parameters + default_config = get_default_config() + action_space = request.args.get('action_space', default_config['action_space']) + observation_type = request.args.get('observation_type', default_config['observation_type']) + model_name = request.args.get('model_name', default_config['model_name']) + + results_path = os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name) + recording_path = os.path.join(results_path, task_type, task_id, "recording.mp4") if os.path.exists(recording_path): response = send_file(recording_path, mimetype='video/mp4') # Add headers to improve mobile compatibility @@ -471,8 +597,14 @@ def task_recording(task_type, task_id): @app.route('/api/task//') def api_task_detail(task_type, task_id): """Task detail API""" + # Get config from URL parameters + default_config = get_default_config() + action_space = request.args.get('action_space', default_config['action_space']) + observation_type = request.args.get('observation_type', default_config['observation_type']) + model_name = request.args.get('model_name', default_config['model_name']) + task_info = get_task_info(task_type, task_id) - task_status = get_task_status(task_type, task_id) + task_status = get_task_status_with_config(task_type, task_id, action_space, observation_type, model_name) if not task_info: return jsonify({"error": "Task does not exist"}), 404 @@ -488,9 +620,9 @@ def api_config(): config_info = { "task_config_path": TASK_CONFIG_PATH, "results_base_path": RESULTS_BASE_PATH, - "action_space": ACTION_SPACE, - "observation_type": OBSERVATION_TYPE, - "model_name": MODEL_NAME, + "action_space": get_default_config()['action_space'], + "observation_type": get_default_config()['observation_type'], + "model_name": get_default_config()['model_name'], "max_steps": MAX_STEPS, "examples_base_path": EXAMPLES_BASE_PATH } @@ -529,16 +661,27 @@ def api_available_configs(): @app.route('/api/current-config') def api_current_config(): """Get current configuration including args.json data""" + # Get config from URL parameters or use defaults + default_config = get_default_config() + action_space = request.args.get('action_space', default_config['action_space']) + observation_type = request.args.get('observation_type', default_config['observation_type']) + model_name = request.args.get('model_name', default_config['model_name']) + + # Get max_steps from args.json if available + model_args = get_model_args(action_space, observation_type, model_name) + max_steps = MAX_STEPS + if model_args and 'max_steps' in model_args: + max_steps = model_args['max_steps'] + config = { - "action_space": ACTION_SPACE, - "observation_type": OBSERVATION_TYPE, - "model_name": MODEL_NAME, - "max_steps": MAX_STEPS, - "results_path": RESULTS_PATH + "action_space": action_space, + "observation_type": observation_type, + "model_name": model_name, + "max_steps": max_steps, + "results_path": os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name) } # Add model args from args.json - model_args = get_model_args(ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME) if model_args: config["model_args"] = model_args else: @@ -546,39 +689,6 @@ def api_current_config(): return jsonify(config) -@app.route('/api/set-config', methods=['POST']) -def api_set_config(): - """Set current configuration""" - global ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME, RESULTS_PATH, MAX_STEPS - - data = request.get_json() - if not data: - return jsonify({"error": "No data provided"}), 400 - - # Update global variables - ACTION_SPACE = data.get('action_space', ACTION_SPACE) - OBSERVATION_TYPE = data.get('observation_type', OBSERVATION_TYPE) - MODEL_NAME = data.get('model_name', MODEL_NAME) - - # Update results path - RESULTS_PATH = os.path.join(RESULTS_BASE_PATH, ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME) - - # Update max_steps from args.json if available - model_args = get_model_args(ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME) - if model_args and 'max_steps' in model_args: - MAX_STEPS = model_args['max_steps'] - - if RESULTS_PATH not in TASK_STATUS_CACHE: - # Initialize cache for this results path - TASK_STATUS_CACHE[RESULTS_PATH] = {} - - return jsonify({ - "action_space": ACTION_SPACE, - "observation_type": OBSERVATION_TYPE, - "model_name": MODEL_NAME, - "max_steps": MAX_STEPS, - "results_path": RESULTS_PATH - }) def get_model_args(action_space, observation_type, model_name): """Get model arguments from args.json file""" @@ -591,6 +701,28 @@ def get_model_args(action_space, observation_type, model_name): print(f"Error reading args.json: {e}") return None +@app.route('/api/clear-cache', methods=['POST']) +def api_clear_cache(): + """Clear task status cache for current configuration""" + global TASK_STATUS_CACHE + + # Get config from URL parameters or use defaults + default_config = get_default_config() + action_space = request.args.get('action_space', default_config['action_space']) + observation_type = request.args.get('observation_type', default_config['observation_type']) + model_name = request.args.get('model_name', default_config['model_name']) + + results_path = os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name) + + # Clear cache only for the current configuration + if results_path in TASK_STATUS_CACHE: + TASK_STATUS_CACHE[results_path].clear() + message = f"Cache cleared for configuration: {action_space}/{observation_type}/{model_name}" + else: + message = f"No cache found for configuration: {action_space}/{observation_type}/{model_name}" + + return jsonify({"message": message}) + if __name__ == '__main__': # Check if necessary directories exist if not os.path.exists(TASK_CONFIG_PATH): diff --git a/monitor/static/index.css b/monitor/static/index.css index 215bcbf..6bb3892 100644 --- a/monitor/static/index.css +++ b/monitor/static/index.css @@ -364,33 +364,6 @@ h2 { color: #0056b3; margin-top: 32px; font-size: 1.6em; } .progress-fill { height: 100%; background: linear-gradient(90deg, #007bff, #00c6ff); width: 0%; transition: width 0.6s ease; } .task-actions { margin-top: 20px; } .timestamp { font-size: 0.9em; color: #6c757d; margin-top: 6px; } -.fab { - position: fixed; - z-index: 1000; - right: 36px; - width: 60px; - height: 60px; - border-radius: 50%; - box-shadow: 0 6px 24px rgba(0,0,0,0.18); - display: flex; - align-items: center; - justify-content: center; - font-size: 1.6em; - font-weight: bold; - background: linear-gradient(135deg, #007bff, #0056b3); - color: #fff; - border: none; - cursor: pointer; - transition: all 0.3s; -} -.fab:hover { - background: linear-gradient(135deg, #0056b3, #007bff); - box-shadow: 0 8px 32px rgba(0,123,255,0.28); - transform: translateY(-3px) rotate(360deg); -} -.fab-refresh { - bottom: 36px; -} .no-tasks { color: #8492a6; @@ -425,16 +398,12 @@ h2 { color: #0056b3; margin-top: 32px; font-size: 1.6em; } margin-top: 4px; } -/* 为列表设置淡入动画 */ @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } - @media (max-width: 600px) { - .fab { right: 18px; width: 52px; height: 52px; font-size: 1.3em; } - .fab-refresh { bottom: 18px; } h1 { font-size: 1.8em; } .system-status { font-size: 0.4em; display: block; margin: 10px auto; width: fit-content; } .task-status { padding: 4px 10px; font-size: 0.85em; } diff --git a/monitor/static/index.js b/monitor/static/index.js index 2c61b3b..d5eed39 100644 --- a/monitor/static/index.js +++ b/monitor/static/index.js @@ -16,6 +16,41 @@ let availableConfigs = []; let currentConfig = null; let categoryStats = {}; +// Get configuration from URL parameters +function getConfigFromURL() { + const urlParams = new URLSearchParams(window.location.search); + return { + action_space: urlParams.get('action_space'), + observation_type: urlParams.get('observation_type'), + model_name: urlParams.get('model_name') + }; +} + +// Update URL parameters with current configuration +function updateURLWithConfig(config) { + const url = new URL(window.location); + if (config.action_space) url.searchParams.set('action_space', config.action_space); + else url.searchParams.delete('action_space'); + if (config.observation_type) url.searchParams.set('observation_type', config.observation_type); + else url.searchParams.delete('observation_type'); + if (config.model_name) url.searchParams.set('model_name', config.model_name); + else url.searchParams.delete('model_name'); + + window.history.replaceState({}, '', url); +} + +// Build API URL with config parameters +function buildAPIURL(endpoint, config = null) { + const params = new URLSearchParams(); + const configToUse = config || getConfigFromURL(); + + if (configToUse.action_space) params.set('action_space', configToUse.action_space); + if (configToUse.observation_type) params.set('observation_type', configToUse.observation_type); + if (configToUse.model_name) params.set('model_name', configToUse.model_name); + + return params.toString() ? `${endpoint}?${params.toString()}` : endpoint; +} + function refreshPage() { // Save expanded state before refresh const expandedTaskTypes = []; @@ -29,128 +64,12 @@ function refreshPage() { // Store in sessionStorage sessionStorage.setItem('expandedTaskTypes', JSON.stringify(expandedTaskTypes)); - // Only fetch brief data for update to improve refresh speed - fetchTasksForRefresh(); -} - -function fetchTasksForRefresh() { - fetch('/api/tasks/brief') - .then(response => response.json()) - .then(data => { - allTaskData = data; - categoryStats = calculateCategoryStats(data); - // Only update statistics and task status, do not fully re-render - updateStatistics(data); - updateTaskStatus(data); - }) - .catch(error => console.error('Error refreshing tasks:', error)); -} - -// New function: only update task status, do not re-render the entire list -function updateTaskStatus(data) { - // Add pulse animation to score banner when refreshing - const scoreBanner = document.querySelector('.score-banner'); - if (scoreBanner) { - scoreBanner.classList.add('refreshing'); - setTimeout(() => { - scoreBanner.classList.remove('refreshing'); - }, 1000); - } - - // Update the status display of each task - Object.entries(data).forEach(([taskType, tasks]) => { - tasks.forEach(task => { - // Find the corresponding task card - const taskCard = document.querySelector(`.task-card[data-task-id="${task.id}"][data-task-type="${taskType}"]`); - if (!taskCard) return; - - // Update status display - const statusElement = taskCard.querySelector('.task-status'); - if (statusElement) { - // Remove all status classes - statusElement.classList.remove('status-not-started', 'status-preparing', 'status-running', 'status-completed', 'status-error', 'status-unknown'); - - // Set new status class and icon - let statusClass = ''; - let statusIcon = ''; - - switch(task.status.status) { - case 'Not Started': - statusClass = 'status-not-started'; - statusIcon = 'fa-hourglass-start'; - break; - case 'Preparing': - case 'Initializing': - statusClass = 'status-preparing'; - statusIcon = 'fa-spinner fa-pulse'; - break; - case 'Running': - statusClass = 'status-running'; - statusIcon = 'fa-running'; - break; - case 'Done': - case 'Done (Message Exit)': - case 'Done (Max Steps)': - case 'Done (Thought Exit)': - statusClass = 'status-completed'; - statusIcon = 'fa-check-circle'; - break; - case 'Error': - statusClass = 'status-error'; - statusIcon = 'fa-exclamation-circle'; - break; - default: - statusClass = 'status-unknown'; - statusIcon = 'fa-question-circle'; - break; - } - - statusElement.classList.add(statusClass); - statusElement.innerHTML = ` ${task.status.status}`; - } - - // Update progress bar - if (task.status.progress > 0) { - const progressText = taskCard.querySelector('.task-details div:first-child'); - if (progressText) { - progressText.innerHTML = ` Progress: ${task.status.progress}/${task.status.max_steps} step(s)`; - } - - const progressFill = taskCard.querySelector('.progress-fill'); - if (progressFill) { - const percentage = (task.status.progress / task.status.max_steps) * 100; - progressFill.style.width = `${percentage}%`; - } - - const progressPercentage = taskCard.querySelector('.progress-percentage'); - if (progressPercentage) { - const percentage = (task.status.progress / task.status.max_steps) * 100; - progressPercentage.textContent = `${Math.round(percentage)}%`; - } - } - - // Update last update time - const timestamp = taskCard.querySelector('.timestamp'); - if (timestamp && task.status.last_update) { - timestamp.innerHTML = ` Last Update: ${task.status.last_update}`; - } - - // Update result info - if (task.status.result) { - let resultDiv = taskCard.querySelector('.task-result'); - if (!resultDiv) { - resultDiv = document.createElement('div'); - resultDiv.className = 'task-result'; - taskCard.querySelector('.task-details').appendChild(resultDiv); - } - resultDiv.innerHTML = ` Result: ${task.status.result}`; - } - }); - }); + // Full page refresh + window.location.reload(); } function fetchTasks() { - fetch('/api/tasks/brief') + fetch(buildAPIURL('/api/tasks/brief')) .then(response => response.json()) .then(data => { allTaskData = data; @@ -428,7 +347,16 @@ function renderTasks(data) { if (task.status.status !== 'Not Started') { taskCard.style.cursor = 'pointer'; taskCard.addEventListener('click', () => { - window.location.href = `/task/${taskType}/${task.id}`; + const config = getConfigFromURL(); + const params = new URLSearchParams(); + if (config.action_space) params.set('action_space', config.action_space); + if (config.observation_type) params.set('observation_type', config.observation_type); + if (config.model_name) params.set('model_name', config.model_name); + + const url = params.toString() ? + `/task/${taskType}/${task.id}?${params.toString()}` : + `/task/${taskType}/${task.id}`; + window.location.href = url; }); } tasksContainer.appendChild(taskCard); @@ -511,40 +439,51 @@ function changeConfiguration() { const selectedConfig = availableConfigs[selectedIndex]; - // Send configuration change request - fetch('/api/set-config', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(selectedConfig) - }) - .then(response => response.json()) - .then(data => { - currentConfig = data; - displayConfig(data); - // Refresh tasks with new configuration - fetchTasks(); - }) - .catch(error => { - console.error('Error setting config:', error); - displayConfigError(); - }); + // Update URL parameters and do full page refresh + updateURLWithConfig(selectedConfig); + window.location.reload(); } function fetchConfig() { - return fetch('/api/current-config') - .then(response => response.json()) - .then(data => { - currentConfig = data; - displayConfig(data); - updateConfigSelect(); - return data; - }) - .catch(error => { - console.error('Error fetching config:', error); - displayConfigError(); - }); + // Check URL parameters first + const urlConfig = getConfigFromURL(); + + if (urlConfig.action_space && urlConfig.observation_type && urlConfig.model_name) { + // Use config from URL and fetch detailed info + const params = new URLSearchParams(); + params.set('action_space', urlConfig.action_space); + params.set('observation_type', urlConfig.observation_type); + params.set('model_name', urlConfig.model_name); + + return fetch(`/api/current-config?${params.toString()}`) + .then(response => response.json()) + .then(data => { + currentConfig = data; + displayConfig(data); + updateConfigSelect(); + return data; + }) + .catch(error => { + console.error('Error fetching config:', error); + displayConfigError(); + }); + } else { + // Fallback to default config from server + return fetch('/api/current-config') + .then(response => response.json()) + .then(data => { + currentConfig = data; + displayConfig(data); + updateConfigSelect(); + // Update URL with current config + updateURLWithConfig(data); + return data; + }) + .catch(error => { + console.error('Error fetching config:', error); + displayConfigError(); + }); + } } function updateConfigSelect() { @@ -574,21 +513,39 @@ function displayConfig(config) { document.getElementById('model-name').textContent = config.model_name || 'N/A'; document.getElementById('max-steps').textContent = config.max_steps || 'N/A'; - // Display model args from args.json + // Display additional model args from args.json (excluding main config params) const modelArgsElement = document.getElementById('model-args'); if (config.model_args && Object.keys(config.model_args).length > 0) { - let argsHtml = ''; - Object.entries(config.model_args).forEach(([key, value]) => { - // Skip max_steps as it's already displayed above - if (key !== 'max_steps') { - argsHtml += `
- ${key}: - ${JSON.stringify(value)} + // Skip the main config parameters that are already displayed + const skipKeys = ['action_space', 'observation_type', 'model_name', 'max_steps']; + const additionalArgs = Object.entries(config.model_args).filter(([key]) => !skipKeys.includes(key)); + + if (additionalArgs.length > 0) { + let argsHtml = ` +
+
+ + Additional Parameters (${additionalArgs.length}) +
+
`; + + additionalArgs.forEach(([key, value]) => { + argsHtml += ` +
+ ${key}: + ${JSON.stringify(value)} +
`; + }); + + argsHtml += ` +
`; - } - }); - modelArgsElement.innerHTML = argsHtml; - modelArgsElement.style.display = 'block'; + + modelArgsElement.innerHTML = argsHtml; + modelArgsElement.style.display = 'block'; + } else { + modelArgsElement.style.display = 'none'; + } } else { modelArgsElement.style.display = 'none'; } @@ -665,3 +622,55 @@ function calculateCategoryStats(data) { return stats; } + +function toggleConfigArgs() { + const content = document.getElementById('config-args-content'); + const chevron = document.getElementById('config-args-chevron'); + + if (content.style.display === 'none' || !content.style.display) { + content.style.display = 'block'; + chevron.classList.remove('fa-chevron-right'); + chevron.classList.add('fa-chevron-down'); + } else { + content.style.display = 'none'; + chevron.classList.remove('fa-chevron-down'); + chevron.classList.add('fa-chevron-right'); + } +} + +function clearCacheAndRefresh() { + if (!confirm('Clearing the cache will cause slower loading temporarily as data needs to be reloaded. Continue?')) { + return; + } + + const button = document.getElementById('clear-cache-btn'); + const originalText = button.innerHTML; + + // Show loading state + button.innerHTML = ' Clearing...'; + button.disabled = true; + + // Build URL with current configuration parameters + const clearCacheUrl = buildAPIURL('/api/clear-cache'); + + fetch(clearCacheUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + console.log('Cache cleared:', data.message); + // Refresh the page after clearing cache + window.location.reload(); + }) + .catch(error => { + console.error('Error clearing cache:', error); + alert('Failed to clear cache. Please try again.'); + + // Restore button state + button.innerHTML = originalText; + button.disabled = false; + }); +} diff --git a/monitor/static/style.css b/monitor/static/style.css index bd0d13b..551beb9 100644 --- a/monitor/static/style.css +++ b/monitor/static/style.css @@ -16,5 +16,48 @@ h1, h2, h3 { margin-top: 20px; } .task-actions { margin-top: 15px; } .btn { padding: 5px 10px; background-color: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; text-decoration: none; display: inline-block; } .btn:hover { background-color: #0069d9; } +.btn-warning { background-color: #ffc107; color: #212529; } +.btn-warning:hover { background-color: #e0a800; } +.btn:disabled { background-color: #6c757d; cursor: not-allowed; } +.config-actions { margin-top: 15px; padding-top: 10px; border-top: 1px solid #eee; } +.config-actions .btn { width: 100%; } + +/* Collapsible config args styling */ +.config-collapsible { margin-top: 10px; } +.config-collapsible-header { + cursor: pointer; + padding: 8px 0; + border-bottom: 1px solid #eee; + display: flex; + align-items: center; + font-weight: 500; + color: #666; +} +.config-collapsible-header:hover { color: #333; } +.config-collapsible-header i { + margin-right: 8px; + transition: transform 0.2s ease; + font-size: 12px; +} +.config-collapsible-content { + display: none; + padding-top: 10px; +} + +/* Fix config item spacing */ +.config-item { + margin-bottom: 8px; + padding: 4px 0; +} +.config-item:last-child { margin-bottom: 0; } +.config-label { + font-weight: 500; + color: #555; + margin-right: 8px; +} +.config-value { + color: #333; + word-break: break-word; +} .refresh-btn { float: right; margin-top: 10px; } .timestamp { font-size: 0.8em; color: #666; } diff --git a/monitor/templates/index.html b/monitor/templates/index.html index 0b95c36..d51654d 100644 --- a/monitor/templates/index.html +++ b/monitor/templates/index.html @@ -53,6 +53,11 @@
+
+ +
@@ -100,7 +105,6 @@ - \ No newline at end of file