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 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/<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_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/<task_type>/<task_id>/screenshot/<path:filename>')
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/<task_type>/<task_id>/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/<task_type>/<task_id>')
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):

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; }
.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; }

View File

@@ -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 = `<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}`;
}
});
});
// 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 += `<div class="config-item">
<span class="config-label">${key}:</span>
<span class="config-value">${JSON.stringify(value)}</span>
// 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 = `
<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>`;
}
});
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 = '<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; }
.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; }

View File

@@ -53,6 +53,11 @@
<!-- Model args from args.json will be populated here -->
</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>
@@ -100,7 +105,6 @@
</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>
</body>
</html>