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
This commit is contained in:
Zilong Zhou
2025-07-18 01:58:20 +08:00
committed by GitHub
parent e70cf0bd93
commit 66694c663d
5 changed files with 450 additions and 293 deletions

View File

@@ -5,10 +5,8 @@ from functools import cache
import os import os
import json import json
import time import time
import subprocess
from datetime import datetime from datetime import datetime
from pathlib import Path from flask import Flask, jsonify, send_file, request, render_template
from flask import Flask, render_template_string, jsonify, send_file, request, render_template
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -36,15 +34,11 @@ else:
EXAMPLES_BASE_PATH = os.getenv("EXAMPLES_BASE_PATH", "../evaluation_examples/examples") EXAMPLES_BASE_PATH = os.getenv("EXAMPLES_BASE_PATH", "../evaluation_examples/examples")
RESULTS_BASE_PATH = os.getenv("RESULTS_BASE_PATH", "../results") 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")) MAX_STEPS = int(os.getenv("MAX_STEPS", "150"))
def initialize_default_config(): @cache
"""Initialize default configuration from the first available config in results directory""" def get_default_config():
global ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME, RESULTS_PATH, MAX_STEPS """Get the first available configuration from results directory"""
if os.path.exists(RESULTS_BASE_PATH): if os.path.exists(RESULTS_BASE_PATH):
try: try:
# Scan for the first available configuration # Scan for the first available configuration
@@ -57,34 +51,38 @@ def initialize_default_config():
for model_name in os.listdir(obs_path): for model_name in os.listdir(obs_path):
model_path = os.path.join(obs_path, model_name) model_path = os.path.join(obs_path, model_name)
if os.path.isdir(model_path): if os.path.isdir(model_path):
# Use the first available configuration as default # Get max_steps from args.json if available
ACTION_SPACE = action_space
OBSERVATION_TYPE = obs_type
MODEL_NAME = model_name
RESULTS_PATH = model_path
# Read max_steps from args.json if available
model_args = get_model_args(action_space, obs_type, model_name) model_args = get_model_args(action_space, obs_type, model_name)
max_steps = MAX_STEPS
if model_args and 'max_steps' in model_args: 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})") print(f"Found default config: {action_space}/{obs_type}/{model_name} (max_steps: {max_steps})")
return return {
'action_space': action_space,
'observation_type': obs_type,
'model_name': model_name,
'max_steps': max_steps
}
except Exception as e: except Exception as e:
print(f"Error scanning results directory for default config: {e}") print(f"Error scanning results directory for default config: {e}")
# Fallback to original environment-based path if no configs found # Fallback to environment-based config if no configs found
RESULTS_PATH = os.path.join(RESULTS_BASE_PATH, ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME) fallback_config = {
print(f"Using fallback config from environment: {ACTION_SPACE}/{OBSERVATION_TYPE}/{MODEL_NAME} (max_steps: {MAX_STEPS})") '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 def ensure_cache_initialized(action_space, observation_type, model_name):
initialize_default_config() """Ensure cache is initialized for the given configuration"""
results_path = os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name)
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] = {}
if RESULTS_PATH not in TASK_STATUS_CACHE: return results_path
# Initialize cache for this results path
TASK_STATUS_CACHE[RESULTS_PATH] = {}
@cache @cache
def load_task_list(): def load_task_list():
@@ -99,8 +97,16 @@ def get_task_info(task_type, task_id):
return json.load(f) return json.load(f)
return None return None
def get_task_status(task_type, task_id): def get_task_status_with_config(task_type, task_id, action_space, observation_type, model_name):
result_dir = os.path.join(RESULTS_PATH, 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']
result_dir = os.path.join(results_path, task_type, task_id)
if not os.path.exists(result_dir): if not os.path.exists(result_dir):
return { return {
@@ -152,7 +158,7 @@ def get_task_status(task_type, task_id):
log_content = f.readlines() log_content = f.readlines()
last_response = None last_response = None
for i, line in enumerate(log_content): for line in log_content:
# Extract agent responses for each step # Extract agent responses for each step
if "Responses: [" in line: if "Responses: [" in line:
response_text = line.split("Responses: [")[1].strip() response_text = line.split("Responses: [")[1].strip()
@@ -192,7 +198,7 @@ def get_task_status(task_type, task_id):
status = "Done (Message Exit)" status = "Done (Message Exit)"
elif log_data.get("exit_condition") and "thought_exit: True" in log_data.get("exit_condition", ""): elif log_data.get("exit_condition") and "thought_exit: True" in log_data.get("exit_condition", ""):
status = "Done (Thought Exit)" status = "Done (Thought Exit)"
elif len(steps) >= MAX_STEPS: elif len(steps) >= max_steps:
status = "Done (Max Steps)" status = "Done (Max Steps)"
else: else:
status = "Running" status = "Running"
@@ -214,25 +220,41 @@ def get_task_status(task_type, task_id):
return { return {
"status": status, "status": status,
"progress": len(steps), "progress": len(steps),
"max_steps": MAX_STEPS, "max_steps": max_steps,
"last_update": last_update, "last_update": last_update,
"steps": steps, "steps": steps,
"log_data": log_data, "log_data": log_data,
"result": result_content "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. Get brief status info for a task, without detailed step data, for fast homepage loading.
""" """
# Generate cache key based on task type and ID results_path = os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name)
cache_key = f"{task_type}_{task_id}" 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 # Check if the status is already cached
current_time = time.time() current_time = time.time()
last_cache_time = None last_cache_time = None
if cache_key in TASK_STATUS_CACHE[RESULTS_PATH]: 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] cached_status, cached_time = TASK_STATUS_CACHE[results_path][cache_key]
last_cache_time = cached_time last_cache_time = cached_time
# If cached status is "Done", check if it's within the stability period # If cached status is "Done", check if it's within the stability period
if cached_status["status"].startswith("Done"): 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 # For non-Done status (like Error), just return from cache
return cached_status 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): if not os.path.exists(result_dir):
return { return {
"status": "Not Started", "status": "Not Started",
"progress": 0, "progress": 0,
"max_steps": MAX_STEPS, "max_steps": max_steps,
"last_update": None "last_update": None
} }
@@ -265,7 +287,7 @@ def get_task_status_brief(task_type, task_id):
return { return {
"status": "Preparing", "status": "Preparing",
"progress": 0, "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") "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 { return {
"status": "Initializing", "status": "Initializing",
"progress": 0, "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") "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" status = "Error"
# If step count reaches max, consider as done # If step count reaches max, consider as done
if step_count >= MAX_STEPS: if step_count >= max_steps:
status = "Done (Max Steps)" status = "Done (Max Steps)"
# Quickly check exit condition in log file (only last few lines) # 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 pass
# If step count reaches max again (double check) # If step count reaches max again (double check)
if step_count >= MAX_STEPS: if step_count >= max_steps:
status = "Done (Max Steps)" status = "Done (Max Steps)"
# Get last update time # Get last update time
@@ -352,18 +374,34 @@ def get_task_status_brief(task_type, task_id):
status_dict = { status_dict = {
"status": status, "status": status,
"progress": step_count, "progress": step_count,
"max_steps": MAX_STEPS, "max_steps": max_steps,
"last_update": last_update, "last_update": last_update,
"result": result_content "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 # Cache the status if it is done or error
if status.startswith("Done") or status == "Error": if status.startswith("Done") or status == "Error":
current_time = last_cache_time if last_cache_time else current_time 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 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(): def get_all_tasks_status():
task_list = load_task_list() task_list = load_task_list()
result = {} result = {}
@@ -389,6 +427,59 @@ def get_all_tasks_status():
return result 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(): def get_all_tasks_status_brief():
""" """
Get brief status info for all tasks, without detailed step data, for fast homepage loading. Get brief status info for all tasks, without detailed step data, for fast homepage loading.
@@ -423,8 +514,14 @@ def index():
@app.route('/task/<task_type>/<task_id>') @app.route('/task/<task_type>/<task_id>')
def task_detail(task_type, task_id): 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_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: if not task_info:
return "Task not found", 404 return "Task not found", 404
@@ -433,22 +530,44 @@ def task_detail(task_type, task_id):
task_id=task_id, task_id=task_id,
task_type=task_type, task_type=task_type,
task_info=task_info, 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') @app.route('/api/tasks')
def api_tasks(): def api_tasks():
"""Task status API""" """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') @app.route('/api/tasks/brief')
def api_tasks_brief(): def api_tasks_brief():
"""Return brief status info for all tasks, without detailed step data, for fast homepage loading.""" """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/<task_type>/<task_id>/screenshot/<path:filename>') @app.route('/task/<task_type>/<task_id>/screenshot/<path:filename>')
def task_screenshot(task_type, task_id, filename): def task_screenshot(task_type, task_id, filename):
"""Get task screenshot""" """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): if os.path.exists(screenshot_path):
return send_file(screenshot_path, mimetype='image/png') return send_file(screenshot_path, mimetype='image/png')
else: else:
@@ -457,7 +576,14 @@ def task_screenshot(task_type, task_id, filename):
@app.route('/task/<task_type>/<task_id>/recording') @app.route('/task/<task_type>/<task_id>/recording')
def task_recording(task_type, task_id): def task_recording(task_type, task_id):
"""Get task recording video""" """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): if os.path.exists(recording_path):
response = send_file(recording_path, mimetype='video/mp4') response = send_file(recording_path, mimetype='video/mp4')
# Add headers to improve mobile compatibility # Add headers to improve mobile compatibility
@@ -471,8 +597,14 @@ def task_recording(task_type, task_id):
@app.route('/api/task/<task_type>/<task_id>') @app.route('/api/task/<task_type>/<task_id>')
def api_task_detail(task_type, task_id): def api_task_detail(task_type, task_id):
"""Task detail API""" """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_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: if not task_info:
return jsonify({"error": "Task does not exist"}), 404 return jsonify({"error": "Task does not exist"}), 404
@@ -488,9 +620,9 @@ def api_config():
config_info = { config_info = {
"task_config_path": TASK_CONFIG_PATH, "task_config_path": TASK_CONFIG_PATH,
"results_base_path": RESULTS_BASE_PATH, "results_base_path": RESULTS_BASE_PATH,
"action_space": ACTION_SPACE, "action_space": get_default_config()['action_space'],
"observation_type": OBSERVATION_TYPE, "observation_type": get_default_config()['observation_type'],
"model_name": MODEL_NAME, "model_name": get_default_config()['model_name'],
"max_steps": MAX_STEPS, "max_steps": MAX_STEPS,
"examples_base_path": EXAMPLES_BASE_PATH "examples_base_path": EXAMPLES_BASE_PATH
} }
@@ -529,16 +661,27 @@ def api_available_configs():
@app.route('/api/current-config') @app.route('/api/current-config')
def api_current_config(): def api_current_config():
"""Get current configuration including args.json data""" """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 = { config = {
"action_space": ACTION_SPACE, "action_space": action_space,
"observation_type": OBSERVATION_TYPE, "observation_type": observation_type,
"model_name": MODEL_NAME, "model_name": model_name,
"max_steps": MAX_STEPS, "max_steps": max_steps,
"results_path": RESULTS_PATH "results_path": os.path.join(RESULTS_BASE_PATH, action_space, observation_type, model_name)
} }
# Add model args from args.json # Add model args from args.json
model_args = get_model_args(ACTION_SPACE, OBSERVATION_TYPE, MODEL_NAME)
if model_args: if model_args:
config["model_args"] = model_args config["model_args"] = model_args
else: else:
@@ -546,39 +689,6 @@ def api_current_config():
return jsonify(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): def get_model_args(action_space, observation_type, model_name):
"""Get model arguments from args.json file""" """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}") print(f"Error reading args.json: {e}")
return None 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__': if __name__ == '__main__':
# Check if necessary directories exist # Check if necessary directories exist
if not os.path.exists(TASK_CONFIG_PATH): if not os.path.exists(TASK_CONFIG_PATH):

View File

@@ -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; } .progress-fill { height: 100%; background: linear-gradient(90deg, #007bff, #00c6ff); width: 0%; transition: width 0.6s ease; }
.task-actions { margin-top: 20px; } .task-actions { margin-top: 20px; }
.timestamp { font-size: 0.9em; color: #6c757d; margin-top: 6px; } .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 { .no-tasks {
color: #8492a6; color: #8492a6;
@@ -425,16 +398,12 @@ h2 { color: #0056b3; margin-top: 32px; font-size: 1.6em; }
margin-top: 4px; margin-top: 4px;
} }
/* 为列表设置淡入动画 */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); } from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
@media (max-width: 600px) { @media (max-width: 600px) {
.fab { right: 18px; width: 52px; height: 52px; font-size: 1.3em; }
.fab-refresh { bottom: 18px; }
h1 { font-size: 1.8em; } h1 { font-size: 1.8em; }
.system-status { font-size: 0.4em; display: block; margin: 10px auto; width: fit-content; } .system-status { font-size: 0.4em; display: block; margin: 10px auto; width: fit-content; }
.task-status { padding: 4px 10px; font-size: 0.85em; } .task-status { padding: 4px 10px; font-size: 0.85em; }

View File

@@ -16,6 +16,41 @@ let availableConfigs = [];
let currentConfig = null; let currentConfig = null;
let categoryStats = {}; 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() { function refreshPage() {
// Save expanded state before refresh // Save expanded state before refresh
const expandedTaskTypes = []; const expandedTaskTypes = [];
@@ -29,128 +64,12 @@ function refreshPage() {
// Store in sessionStorage // Store in sessionStorage
sessionStorage.setItem('expandedTaskTypes', JSON.stringify(expandedTaskTypes)); sessionStorage.setItem('expandedTaskTypes', JSON.stringify(expandedTaskTypes));
// Only fetch brief data for update to improve refresh speed // Full page refresh
fetchTasksForRefresh(); window.location.reload();
}
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 = `<i class="fas ${statusIcon}"></i> ${task.status.status}`;
}
// Update progress bar
if (task.status.progress > 0) {
const progressText = taskCard.querySelector('.task-details div:first-child');
if (progressText) {
progressText.innerHTML = `<i class="fas fa-chart-line"></i> 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 = `<i class="far fa-clock"></i> 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 = `<strong><i class="fas fa-flag-checkered"></i> Result:</strong> ${task.status.result}`;
}
});
});
} }
function fetchTasks() { function fetchTasks() {
fetch('/api/tasks/brief') fetch(buildAPIURL('/api/tasks/brief'))
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
allTaskData = data; allTaskData = data;
@@ -428,7 +347,16 @@ function renderTasks(data) {
if (task.status.status !== 'Not Started') { if (task.status.status !== 'Not Started') {
taskCard.style.cursor = 'pointer'; taskCard.style.cursor = 'pointer';
taskCard.addEventListener('click', () => { 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); tasksContainer.appendChild(taskCard);
@@ -511,40 +439,51 @@ function changeConfiguration() {
const selectedConfig = availableConfigs[selectedIndex]; const selectedConfig = availableConfigs[selectedIndex];
// Send configuration change request // Update URL parameters and do full page refresh
fetch('/api/set-config', { updateURLWithConfig(selectedConfig);
method: 'POST', window.location.reload();
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();
});
} }
function fetchConfig() { function fetchConfig() {
return fetch('/api/current-config') // Check URL parameters first
.then(response => response.json()) const urlConfig = getConfigFromURL();
.then(data => {
currentConfig = data; if (urlConfig.action_space && urlConfig.observation_type && urlConfig.model_name) {
displayConfig(data); // Use config from URL and fetch detailed info
updateConfigSelect(); const params = new URLSearchParams();
return data; params.set('action_space', urlConfig.action_space);
}) params.set('observation_type', urlConfig.observation_type);
.catch(error => { params.set('model_name', urlConfig.model_name);
console.error('Error fetching config:', error);
displayConfigError(); 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() { function updateConfigSelect() {
@@ -574,21 +513,39 @@ function displayConfig(config) {
document.getElementById('model-name').textContent = config.model_name || 'N/A'; document.getElementById('model-name').textContent = config.model_name || 'N/A';
document.getElementById('max-steps').textContent = config.max_steps || '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'); const modelArgsElement = document.getElementById('model-args');
if (config.model_args && Object.keys(config.model_args).length > 0) { if (config.model_args && Object.keys(config.model_args).length > 0) {
let argsHtml = ''; // Skip the main config parameters that are already displayed
Object.entries(config.model_args).forEach(([key, value]) => { const skipKeys = ['action_space', 'observation_type', 'model_name', 'max_steps'];
// Skip max_steps as it's already displayed above const additionalArgs = Object.entries(config.model_args).filter(([key]) => !skipKeys.includes(key));
if (key !== 'max_steps') {
argsHtml += `<div class="config-item"> if (additionalArgs.length > 0) {
<span class="config-label">${key}:</span> let argsHtml = `
<span class="config-value">${JSON.stringify(value)}</span> <div class="config-collapsible">
<div class="config-collapsible-header" onclick="toggleConfigArgs()">
<i class="fas fa-chevron-right" id="config-args-chevron"></i>
<span>Additional Parameters (${additionalArgs.length})</span>
</div>
<div class="config-collapsible-content" id="config-args-content">`;
additionalArgs.forEach(([key, value]) => {
argsHtml += `
<div class="config-item">
<span class="config-label">${key}:</span>
<span class="config-value">${JSON.stringify(value)}</span>
</div>`;
});
argsHtml += `
</div>
</div>`; </div>`;
}
}); modelArgsElement.innerHTML = argsHtml;
modelArgsElement.innerHTML = argsHtml; modelArgsElement.style.display = 'block';
modelArgsElement.style.display = 'block'; } else {
modelArgsElement.style.display = 'none';
}
} else { } else {
modelArgsElement.style.display = 'none'; modelArgsElement.style.display = 'none';
} }
@@ -665,3 +622,55 @@ function calculateCategoryStats(data) {
return stats; 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 = '<i class="fas fa-spinner fa-spin"></i> 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;
});
}

View File

@@ -16,5 +16,48 @@ h1, h2, h3 { margin-top: 20px; }
.task-actions { margin-top: 15px; } .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 { 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: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; } .refresh-btn { float: right; margin-top: 10px; }
.timestamp { font-size: 0.8em; color: #666; } .timestamp { font-size: 0.8em; color: #666; }

View File

@@ -53,6 +53,11 @@
<!-- Model args from args.json will be populated here --> <!-- Model args from args.json will be populated here -->
</div> </div>
</div> </div>
<div class="config-actions">
<button id="clear-cache-btn" class="btn btn-warning" onclick="clearCacheAndRefresh()">
<i class="fas fa-broom"></i> Clear Cache
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -100,7 +105,6 @@
</div> </div>
</div> </div>
</div> </div>
<button class="fab fab-refresh" onclick="refreshPage()" title="Refresh"><i class="fas fa-sync-alt"></i></button>
<script src="/static/index.js"></script> <script src="/static/index.js"></script>
</body> </body>
</html> </html>