diff --git a/monitor/Dockerfile b/monitor/Dockerfile new file mode 100644 index 0000000..4692b7f --- /dev/null +++ b/monitor/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.9-slim + +WORKDIR /app + +# Install dependencies +COPY monitor/requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY monitor/ ./ + +# Expose port (will be overridden by environment variable) +ARG FLASK_PORT=8080 +EXPOSE ${FLASK_PORT} + +# Command to run the application +CMD ["python", "main.py"] diff --git a/monitor/docker-compose.yml b/monitor/docker-compose.yml new file mode 100644 index 0000000..a03c56c --- /dev/null +++ b/monitor/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' + +services: + monitor: + build: + context: .. + dockerfile: monitor/Dockerfile + ports: + - "${FLASK_PORT:-8080}:${FLASK_PORT:-8080}" + volumes: + - ../evaluation_examples:/app/evaluation_examples + - ../results_operator_aws:/app/results_operator_aws + env_file: + - .env + environment: + - FLASK_ENV=production + restart: unless-stopped diff --git a/monitor/main.py b/monitor/main.py new file mode 100644 index 0000000..fa83ca0 --- /dev/null +++ b/monitor/main.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import json +import time +from datetime import datetime +from pathlib import Path +from flask import Flask, render_template_string, jsonify, send_file, request, render_template +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +app = Flask(__name__) + +# Load configuration from environment variables +TASK_CONFIG_PATH = os.getenv("TASK_CONFIG_PATH", "evaluation_examples/test_small.json") +EXAMPLES_BASE_PATH = os.getenv("EXAMPLES_BASE_PATH", "evaluation_examples/examples") +RESULTS_BASE_PATH = os.getenv("RESULTS_BASE_PATH", "results_operator_aws/pyautogui/screenshot/computer-use-preview") +MAX_STEPS = int(os.getenv("MAX_STEPS", "50")) + +def load_task_list(): + with open(TASK_CONFIG_PATH, 'r') as f: + return json.load(f) + +def get_task_info(task_type, task_id): + task_file = os.path.join(EXAMPLES_BASE_PATH, task_type, f"{task_id}.json") + if os.path.exists(task_file): + with open(task_file, 'r') as f: + return json.load(f) + return None + +def get_task_status(task_type, task_id): + result_dir = os.path.join(RESULTS_BASE_PATH, task_type, task_id) + + if not os.path.exists(result_dir): + return { + "status": "Not Started", + "progress": 0, + "total_steps": 0, + "last_update": None + } + + traj_file = os.path.join(result_dir, "traj.jsonl") + log_file = os.path.join(result_dir, "runtime.log") + result_file = os.path.join(result_dir, "result.txt") + + if not os.path.exists(traj_file): + return { + "status": "Preparing", + "progress": 0, + "total_steps": 0, + "last_update": datetime.fromtimestamp(os.path.getmtime(result_dir)).strftime("%Y-%m-%d %H:%M:%S") + } + + # read trajectory file + steps = [] + with open(traj_file, 'r') as f: + for line in f: + if line.strip(): + steps.append(json.loads(line)) + + if not steps: + return { + "status": "Initializing", + "progress": 0, + "total_steps": 0, + "last_update": datetime.fromtimestamp(os.path.getmtime(traj_file)).strftime("%Y-%m-%d %H:%M:%S") + } + + last_step = steps[-1] + + # check if the task is done + if last_step.get("done", False): + status = "Done" + elif last_step.get("Error", False): + status = "Error" + else: + status = "Running" + + # get last action timestamp + try: + last_update = datetime.strptime(last_step["action_timestamp"], "%Y%m%d@%H%M%S").strftime("%Y-%m-%d %H:%M:%S") + except KeyError: + last_update = "None" + + result_content = "Task not completed" + if status == "Done": + if os.path.exists(result_file): + with open(result_file, 'r') as f: + result_content = f.read().strip() + else: + result_content = "Result file not found" + + return { + "status": status, + "progress": len(steps), + "max_steps": MAX_STEPS, + "last_update": last_update, + "steps": steps, + "result": result_content + } + +def get_all_tasks_status(): + 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(task_type, task_id) + + 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 + +@app.route('/') +def index(): + return render_template("index.html") + +@app.route('/task//') +def task_detail(task_type, task_id): + task_info = get_task_info(task_type, task_id) + task_status = get_task_status(task_type, task_id) + + if not task_info: + return "Task not found", 404 + + return render_template("task_detail.html", + task_id=task_id, + task_type=task_type, + task_info=task_info, + task_status=task_status) + +@app.route('/api/tasks') +def api_tasks(): + """Task status API""" + return jsonify(get_all_tasks_status()) + +@app.route('/task///screenshot/') +def task_screenshot(task_type, task_id, filename): + """Get task screenshot""" + screenshot_path = os.path.join(RESULTS_BASE_PATH, task_type, task_id, filename) + if os.path.exists(screenshot_path): + return send_file(screenshot_path, mimetype='image/png') + else: + return "Screenshot does not exist", 404 + +@app.route('/api/task//') +def api_task_detail(task_type, task_id): + """Task detail API""" + task_info = get_task_info(task_type, task_id) + task_status = get_task_status(task_type, task_id) + + if not task_info: + return jsonify({"error": "Task does not exist"}), 404 + + return jsonify({ + "info": task_info, + "status": task_status + }) + +if __name__ == '__main__': + # Check if necessary directories exist + if not os.path.exists(TASK_CONFIG_PATH): + print(f"Warning: Task config file does not exist: {TASK_CONFIG_PATH}") + + if not os.path.exists(EXAMPLES_BASE_PATH): + print(f"Warning: Task examples directory does not exist: {EXAMPLES_BASE_PATH}") + + # Start web service + host = os.getenv("FLASK_HOST", "0.0.0.0") + port = int(os.getenv("FLASK_PORT", "8080")) + debug = os.getenv("FLASK_DEBUG", "false").lower() == "true" + + app.run(host=host, port=port, debug=debug) \ No newline at end of file diff --git a/monitor/requirements.txt b/monitor/requirements.txt new file mode 100644 index 0000000..1386bd7 --- /dev/null +++ b/monitor/requirements.txt @@ -0,0 +1,2 @@ +Flask~=3.0.0 +python-dotenv==0.19.0 diff --git a/monitor/static/.gitignore b/monitor/static/.gitignore new file mode 100644 index 0000000..63f9634 --- /dev/null +++ b/monitor/static/.gitignore @@ -0,0 +1,4 @@ +# Override parent .gitignore rules +# This allows all files in this directory to be tracked +!* +!*/ diff --git a/monitor/static/index.css b/monitor/static/index.css new file mode 100644 index 0000000..87a3a28 --- /dev/null +++ b/monitor/static/index.css @@ -0,0 +1,304 @@ +/* filepath: /home/adlsdztony/codes/OSWorld/monitor/static/index.css */ +body { font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 0; background: linear-gradient(135deg, #f4f6fa 0%, #e9f0f9 100%); } +.main-container { max-width: 1100px; margin: 40px auto; background: #fff; border-radius: 14px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); padding: 36px 44px; } +h1 { font-size: 2.5em; margin-bottom: 24px; color: #1a237e; text-align: center; position: relative; } +h1:after { content: ''; display: block; width: 80px; height: 4px; background: linear-gradient(90deg, #007bff, #00c6ff); margin: 12px auto 0; border-radius: 2px; } +h2 { color: #0056b3; margin-top: 32px; font-size: 1.6em; } + +.system-status { + font-size: 0.4em; + vertical-align: middle; + padding: 5px 12px; + border-radius: 20px; + margin-left: 10px; + font-weight: normal; + display: inline-block; + animation: pulse 2s infinite; +} +.system-status.online { + background: linear-gradient(135deg, #d4edda, #c3e6cb); + color: #155724; +} +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(21, 87, 36, 0.4); } + 70% { box-shadow: 0 0 0 10px rgba(21, 87, 36, 0); } + 100% { box-shadow: 0 0 0 0 rgba(21, 87, 36, 0); } +} + +.dashboard-stats { + display: flex; + justify-content: space-between; + margin: 30px 0; + flex-wrap: wrap; +} +.stat-card { + flex: 1; + min-width: 220px; + background: linear-gradient(135deg, #ffffff, #f8faff); + padding: 20px; + margin: 0 10px 20px; + border-radius: 10px; + box-shadow: 0 5px 15px rgba(0,0,0,0.05); + text-align: center; + transition: all 0.4s; + position: relative; + overflow: hidden; + cursor: pointer; +} +.stat-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0,123,255,0.15); + transition: all 0.4s; +} +.stat-card i { + font-size: 2em; + color: #007bff; + margin-bottom: 10px; + display: block; +} +.stat-card span { + font-size: 2em; + font-weight: 600; + color: #1a237e; + display: block; + margin-bottom: 5px; +} +.stat-label { + color: #6c757d; + font-size: 0.9em; +} + +.loading-spinner { + text-align: center; + padding: 40px; + color: #6c757d; +} +.spinner { + border: 4px solid rgba(0,0,0,0.1); + width: 36px; + height: 36px; + border-radius: 50%; + border-left-color: #007bff; + animation: spin 1s linear infinite; + margin: 0 auto 15px; +} +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.btn { padding: 10px 22px; background: linear-gradient(90deg,#007bff,#0056b3); color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; transition: all 0.3s; text-decoration: none; display: inline-block; } +.btn:hover { background: linear-gradient(90deg,#0056b3,#007bff); transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,123,255,0.4); } +.refresh-btn { float: right; margin-top: 10px; } +.task-type { + margin-bottom: 24px; + position: relative; + background: #ffffff; + border-radius: 16px; + box-shadow: 0 6px 24px rgba(0,0,0,0.06); + overflow: hidden; + transition: all 0.4s; +} + +.task-type-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 18px 24px; + background: linear-gradient(90deg, #f8faff 0%, #eef5ff 100%); + border-bottom: 1px solid #e0eaf5; + cursor: pointer; + transition: all 0.3s; +} + +.task-type-header:hover { + background: linear-gradient(90deg, #e9f3ff 0%, #d8eaff 100%); +} + +.task-type-name { + font-size: 1.3em; + font-weight: 600; + color: #0056b3; +} + +.task-type-name i { + margin-right: 12px; +} + +.task-type-stats { + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.task-stat { + font-size: 0.9em; + padding: 4px 10px; + border-radius: 20px; + background: #f0f4f8; + color: #566b8c; +} + +.task-stat i { + margin-right: 5px; +} + +.task-stat.running { + background: #e3f2fd; + color: #0d47a1; +} + +.task-stat.completed { + background: #e8f5e9; + color: #1b5e20; +} + +.task-stat.error { + background: #ffebee; + color: #b71c1c; +} + +.tasks-container { + padding: 20px; + transition: all 0.4s cubic-bezier(.4,0,.2,1); + opacity: 1; + max-height: 2000px; +} + +.task-type.collapsed .tasks-container { + max-height: 0; + opacity: 0; + padding: 0; + overflow: hidden; +} + +.task-type.collapsed .task-type-header { + border-bottom: none; +} + +.task-card { + border: none; + background: #f8faff; + padding: 22px; + margin-bottom: 16px; + border-radius: 12px; + box-shadow: 0 3px 10px rgba(0,0,0,0.04); + transition: all 0.3s ease; + position: relative; + z-index: 2; +} +.task-card:hover { box-shadow: 0 10px 30px rgba(0,123,255,0.12); transform: translateY(-3px); } +.task-header { display: flex; justify-content: space-between; margin-bottom: 14px; align-items: center; } +.task-title { font-size: 1.2em; font-weight: 600; color: #1a237e; } +.task-status { padding: 6px 14px; border-radius: 20px; font-size: 0.9em; font-weight: 600; letter-spacing: 0.5px; box-shadow: 0 2px 6px rgba(0,0,0,0.08); } +.status-not-started { background: linear-gradient(135deg, #f0f0f0, #e6e6e6); color: #555; } +.status-preparing { background: linear-gradient(135deg, #fff7e0, #ffe8a3); color: #8a6d00; } +.status-running { background: linear-gradient(135deg, #e3f2fd, #bbdefb); color: #0d47a1; } +.status-completed { background: linear-gradient(135deg, #e8f5e9, #c8e6c9); color: #1b5e20; } +.status-error { background: linear-gradient(135deg, #ffebee, #ffcdd2); color: #b71c1c; } +.task-details { margin-top: 16px; } +.progress-bar { height: 12px; background-color: #eef2f7; border-radius: 6px; margin-top: 10px; overflow: hidden; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); } +.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; + font-style: italic; + text-align: center; + margin: 20px 0; + font-size: 1.1em; + padding: 24px; + background: #f8fafc; + border-radius: 10px; + border: 1px dashed #c0cfdf; +} + +.task-instruction { + margin-bottom: 12px; + color: #333; + font-size: 1.05em; +} + +.task-result { + margin-top: 12px; + padding: 10px; + background: #f8f9fa; + border-radius: 6px; + border-left: 3px solid #007bff; +} + +.progress-percentage { + text-align: right; + font-size: 0.85em; + color: #6c757d; + margin-top: 4px; +} + +/* 为列表设置淡入动画 */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.task-card { + animation: fadeIn 0.5s ease-out forwards; +} + +/* 确保每个卡片的动画有延迟,创造瀑布效果 */ +.task-card:nth-child(1) { animation-delay: 0.1s; } +.task-card:nth-child(2) { animation-delay: 0.2s; } +.task-card:nth-child(3) { animation-delay: 0.3s; } +.task-card:nth-child(4) { animation-delay: 0.4s; } +.task-card:nth-child(5) { animation-delay: 0.5s; } +.task-card:nth-child(n+6) { animation-delay: 0.6s; } + +@media (max-width: 600px) { + .fab { right: 18px; width: 52px; height: 52px; font-size: 1.3em; } + .fab-refresh { bottom: 18px; } + h1 { font-size: 2em; } + .task-status { padding: 4px 10px; font-size: 0.85em; } + .dashboard-stats { flex-direction: column; } + .stat-card { margin: 0 0 15px; min-width: 100%; } +} + +@media (max-width: 700px) { + .main-container { padding: 20px; margin: 20px; border-radius: 10px; } + .task-card { padding: 16px; } + h1:after { width: 60px; } +} + +.stat-card.selected { + box-shadow: 0 0 0 3px #0078d7, 0 2px 8px rgba(0,0,0,0.08); + border-color: #0078d7; + background: #e6f0fa; + color: #0078d7; +} + diff --git a/monitor/static/index.js b/monitor/static/index.js new file mode 100644 index 0000000..6769bb3 --- /dev/null +++ b/monitor/static/index.js @@ -0,0 +1,281 @@ +// filepath: /home/adlsdztony/codes/OSWorld/monitor/static/index.js +document.addEventListener('DOMContentLoaded', () => { + fetchTasks(); + // 筛选功能绑定 + document.getElementById('total-tasks').parentElement.addEventListener('click', () => setTaskFilter('all')); + document.getElementById('active-tasks').parentElement.addEventListener('click', () => setTaskFilter('active')); + document.getElementById('completed-tasks').parentElement.addEventListener('click', () => setTaskFilter('completed')); +}); + +let allTaskData = null; +let currentFilter = 'all'; + +function refreshPage() { + // Save expanded state before refresh + const expandedTaskTypes = []; + document.querySelectorAll('.task-type').forEach(section => { + if (!section.classList.contains('collapsed')) { + const typeName = section.querySelector('.task-type-name').textContent.trim(); + expandedTaskTypes.push(typeName); + } + }); + + // Store in sessionStorage + sessionStorage.setItem('expandedTaskTypes', JSON.stringify(expandedTaskTypes)); + + fetchTasks(); +} + +function fetchTasks() { + fetch('/api/tasks') + .then(response => response.json()) + .then(data => { + allTaskData = data; + renderTasks(data); + updateStatistics(data); + }) + .catch(error => console.error('Error fetching tasks:', error)); +} + +function setTaskFilter(filter) { + currentFilter = filter; + if (!allTaskData) return; + renderTasks(allTaskData); + // 高亮选中卡片 + document.querySelectorAll('.stat-card').forEach(card => card.classList.remove('selected')); + if (filter === 'all') { + document.getElementById('total-tasks').parentElement.classList.add('selected'); + } else if (filter === 'active') { + document.getElementById('active-tasks').parentElement.classList.add('selected'); + } else if (filter === 'completed') { + document.getElementById('completed-tasks').parentElement.classList.add('selected'); + } +} + +// 更新统计信息 +function updateStatistics(data) { + let totalTasks = 0; + let activeTasks = 0; + let completedTasks = 0; + + Object.entries(data).forEach(([taskType, tasks]) => { + totalTasks += tasks.length; + tasks.forEach(task => { + if (task.status.status === 'Running' || task.status.status === 'Preparing' || task.status.status === 'Initializing') { + activeTasks++; + } else if (task.status.status === 'Done') { + completedTasks++; + } + }); + }); + + document.getElementById('total-tasks').textContent = totalTasks; + document.getElementById('active-tasks').textContent = activeTasks; + document.getElementById('completed-tasks').textContent = completedTasks; +} + +function renderTasks(data) { + const container = document.getElementById('task-container'); + container.innerHTML = ''; + let filteredData = {}; + if (currentFilter === 'all') { + filteredData = data; + } else { + Object.entries(data).forEach(([taskType, tasks]) => { + let filteredTasks = []; + if (currentFilter === 'active') { + filteredTasks = tasks.filter(task => ['Running', 'Preparing', 'Initializing'].includes(task.status.status)); + } else if (currentFilter === 'completed') { + filteredTasks = tasks.filter(task => task.status.status === 'Done'); + } + if (filteredTasks.length > 0) { + filteredData[taskType] = filteredTasks; + } + }); + } + if (Object.keys(filteredData).length === 0) { + container.innerHTML = '
No tasks at the moment
'; + return; + } + + Object.entries(filteredData).forEach(([taskType, tasks]) => { + // Calculate task statistics for this type + let runningCount = 0; + let completedCount = 0; + let errorCount = 0; + + tasks.forEach(task => { + if (task.status.status === 'Running' || task.status.status === 'Preparing' || task.status.status === 'Initializing') { + runningCount++; + } else if (task.status.status === 'Done') { + completedCount++; + } else if (task.status.status === 'Error') { + errorCount++; + } + }); + + // Create the task type card + const typeSection = document.createElement('div'); + typeSection.className = 'task-type'; + + // Create header with task type name and statistics + const typeHeader = document.createElement('div'); + typeHeader.className = 'task-type-header'; + typeHeader.innerHTML = ` + ${taskType} +
+ ${tasks.length} total + ${runningCount} active + ${completedCount} completed + ${errorCount > 0 ? ` ${errorCount} error` : ''} +
+ `; + typeSection.appendChild(typeHeader); + + // Create container for task cards + const tasksContainer = document.createElement('div'); + tasksContainer.className = 'tasks-container'; + + // Set default collapsed state + typeSection.classList.add('collapsed'); + tasksContainer.setAttribute('aria-hidden', 'true'); + + if (tasks.length === 0) { + const noTasks = document.createElement('div'); + noTasks.className = 'no-tasks'; + noTasks.innerHTML = ' No Tasks Available'; + tasksContainer.appendChild(noTasks); + } else { + tasks.forEach(task => { + const taskCard = document.createElement('div'); + taskCard.className = 'task-card'; + + const taskHeader = document.createElement('div'); + taskHeader.className = 'task-header'; + + const taskTitle = document.createElement('div'); + taskTitle.className = 'task-title'; + taskTitle.innerHTML = ` Task ID: ${task.id}`; + taskHeader.appendChild(taskTitle); + + const taskStatus = document.createElement('div'); + taskStatus.className = 'task-status'; + 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': + statusClass = 'status-completed'; + statusIcon = 'fa-check-circle'; + break; + case 'Error': + statusClass = 'status-error'; + statusIcon = 'fa-exclamation-circle'; + break; + } + + taskStatus.classList.add(statusClass); + taskStatus.innerHTML = ` ${task.status.status}`; + taskHeader.appendChild(taskStatus); + taskCard.appendChild(taskHeader); + + const taskInstruction = document.createElement('div'); + taskInstruction.className = 'task-instruction'; + taskInstruction.innerHTML = ` Instruction: ${task.instruction}`; + taskCard.appendChild(taskInstruction); + + const taskProgress = document.createElement('div'); + taskProgress.className = 'task-details'; + + if (task.status.progress > 0) { + const progressText = document.createElement('div'); + progressText.innerHTML = ` Progress: ${task.status.progress} step(s)`; + taskProgress.appendChild(progressText); + + const progressBar = document.createElement('div'); + progressBar.className = 'progress-bar'; + const progressFill = document.createElement('div'); + progressFill.className = 'progress-fill'; + const percentage = (task.status.progress / task.status.max_steps) * 100; + progressFill.style.width = `${percentage}%`; + progressBar.appendChild(progressFill); + taskProgress.appendChild(progressBar); + + const progressPercentage = document.createElement('div'); + progressPercentage.className = 'progress-percentage'; + progressPercentage.textContent = `${Math.round(percentage)}%`; + taskProgress.appendChild(progressPercentage); + } + + if (task.status.last_update) { + const timestamp = document.createElement('div'); + timestamp.className = 'timestamp'; + timestamp.innerHTML = ` Last Update: ${task.status.last_update}`; + taskProgress.appendChild(timestamp); + } + + if (task.status.result) { + const resultDiv = document.createElement('div'); + resultDiv.className = 'task-result'; + resultDiv.innerHTML = ` Result: ${task.status.result}`; + taskProgress.appendChild(resultDiv); + } + + taskCard.appendChild(taskProgress); + + if (task.status.status !== 'Not Started') { + taskCard.style.cursor = 'pointer'; + taskCard.addEventListener('click', () => { + window.location.href = `/task/${taskType}/${task.id}`; + }); + } + tasksContainer.appendChild(taskCard); + }); + } + typeSection.appendChild(tasksContainer); + + // Toggle collapse when clicking on the header + typeHeader.addEventListener('click', (event) => { + // Prevent toggling when clicking task cards + if (!event.target.closest('.task-card')) { + typeSection.classList.toggle('collapsed'); + + // Set appropriate aria attributes for accessibility + const isCollapsed = typeSection.classList.contains('collapsed'); + tasksContainer.setAttribute('aria-hidden', isCollapsed); + + // Update session storage with current expanded state + const expandedTaskTypes = []; + document.querySelectorAll('.task-type').forEach(section => { + if (!section.classList.contains('collapsed')) { + const typeName = section.querySelector('.task-type-name').textContent.trim(); + expandedTaskTypes.push(typeName); + } + }); + sessionStorage.setItem('expandedTaskTypes', JSON.stringify(expandedTaskTypes)); + } + }); + + // Check if this task type was expanded before refresh + const expandedTaskTypes = JSON.parse(sessionStorage.getItem('expandedTaskTypes') || '[]'); + if (expandedTaskTypes.includes(taskType)) { + typeSection.classList.remove('collapsed'); + tasksContainer.setAttribute('aria-hidden', 'false'); + } + + container.appendChild(typeSection); + }); +} diff --git a/monitor/static/style.css b/monitor/static/style.css new file mode 100644 index 0000000..bd0d13b --- /dev/null +++ b/monitor/static/style.css @@ -0,0 +1,20 @@ +body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; } +h1, h2, h3 { margin-top: 20px; } +.task-type { margin-bottom: 30px; } +.task-card { border: 1px solid #ddd; padding: 15px; margin-bottom: 15px; border-radius: 5px; } +.task-header { display: flex; justify-content: space-between; margin-bottom: 10px; } +.task-title { margin: 0; font-size: 1.1em; font-weight: bold; } +.task-status { padding: 3px 8px; border-radius: 3px; font-size: 0.9em; } +.status-not-started { background-color: #f0f0f0; } +.status-preparing { background-color: #fff3cd; } +.status-running { background-color: #cce5ff; } +.status-completed { background-color: #d4edda; } +.status-error { background-color: #f8d7da; color: #721c24; } +.task-details { margin-top: 10px; } +.progress-bar { height: 10px; background-color: #e9ecef; border-radius: 5px; margin-top: 5px; overflow: hidden; } +.progress-fill { height: 100%; background-color: #007bff; width: 0%; } +.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; } +.refresh-btn { float: right; margin-top: 10px; } +.timestamp { font-size: 0.8em; color: #666; } diff --git a/monitor/static/task_detail.css b/monitor/static/task_detail.css new file mode 100644 index 0000000..c399504 --- /dev/null +++ b/monitor/static/task_detail.css @@ -0,0 +1,158 @@ +body { font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 0; background: linear-gradient(135deg, #f4f6fa 0%, #e9f0f9 100%); } +.container { max-width: 950px; margin: 40px auto; background: #fff; border-radius: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.12); padding: 36px 44px; } +h1 { font-size: 2.4em; margin-bottom: 14px; color: #1a237e; position: relative; } +h1:after { content: ''; display: block; width: 70px; height: 4px; background: linear-gradient(90deg, #007bff, #00c6ff); margin: 12px 0 0; border-radius: 2px; } +h2 { color: #0056b3; margin-top: 36px; font-size: 1.6em; font-weight: 600; } +.back-link { + margin-bottom: 28px; + display: inline-block; + color: #fff; + background: linear-gradient(135deg, #007bff, #0056b3); + padding: 10px 24px; + border-radius: 30px; + font-weight: 600; + font-size: 1.1em; + text-decoration: none; + box-shadow: 0 4px 12px rgba(0,123,255,0.18); + transition: all 0.3s; + position: relative; + overflow: hidden; +} +.back-link:before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, rgba(255,255,255,0.2), rgba(255,255,255,0)); + transition: all 0.5s; +} +.back-link:hover { + background: linear-gradient(135deg, #0056b3, #0078d7); + color: #fff; + box-shadow: 0 6px 18px rgba(0,123,255,0.28); + transform: translateY(-2px); +} +.back-link:hover:before { + left: 100%; +} +.btn { padding: 10px 22px; background: linear-gradient(135deg, #007bff, #0056b3); color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; transition: all 0.3s; box-shadow: 0 3px 10px rgba(0,123,255,0.2); } +.btn:hover { background: linear-gradient(135deg, #0056b3, #0078d7); transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,123,255,0.4); } +.refresh-btn { float: right; margin-top: 10px; } +.task-info { margin-bottom: 42px; } +.task-info dl { display: grid; grid-template-columns: 180px 1fr; grid-gap: 16px; background: linear-gradient(135deg, #f8f9fb, #f1f5fa); border-radius: 10px; padding: 22px 28px; box-shadow: 0 3px 15px rgba(0,0,0,0.04); } +.task-info dt { font-weight: 600; color: #1a237e; position: relative; } +.task-info dt:after { content: ':'; position: absolute; right: 10px; color: #bbb; } +.task-info dd { color: #333; font-size: 1.05em; } +.task-steps { margin-top: 42px; } +.step-card { + border: none; + background: #fafdff; + padding: 22px 26px; + margin-bottom: 24px; + border-radius: 10px; + box-shadow: 0 6px 18px rgba(0,0,0,0.06); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} +.step-card:hover { + box-shadow: 0 10px 30px rgba(0,123,255,0.1); + transform: translateY(-3px); +} +.step-card:before { + content: ''; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 4px; + background: linear-gradient(to bottom, #007bff, #00c6ff); +} +.step-header { display: flex; justify-content: space-between; margin-bottom: 12px; align-items: center; } +.step-title { font-weight: 600; color: #1a237e; font-size: 1.1em; } +.step-time { color: #6c757d; font-size: 0.92em; } +pre { + background: linear-gradient(135deg, #f3f6fa, #edf1f7); + padding: 16px; + border-radius: 8px; + overflow-x: auto; + font-size: 1em; + box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); + border-left: 3px solid rgba(0,123,255,0.3); +} +.step-image { + margin-top: 18px; + max-width: 100%; + border: none; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0,0,0,0.08); + transition: all 0.3s; +} +.step-image:hover { + transform: scale(1.01); + box-shadow: 0 8px 25px rgba(0,0,0,0.12); +} +.no-steps { + color: #8492a6; + font-style: italic; + text-align: center; + margin: 36px 0; + font-size: 1.15em; + padding: 30px; + background: #f8fafc; + border-radius: 10px; + border: 1px dashed #c0cfdf; +} +.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); +} +.task-id { + font-size: 0.5em; + background: linear-gradient(135deg, #e3f2fd, #bbdefb); + color: #0d47a1; + padding: 5px 12px; + border-radius: 20px; + font-weight: normal; + vertical-align: middle; + margin-left: 10px; +} + +.status { + display: inline-block; + padding: 6px 14px; + border-radius: 20px; + font-weight: 600; + letter-spacing: 0.5px; + box-shadow: 0 2px 6px rgba(0,0,0,0.08); + width: fit-content; + white-space: nowrap; +} + +.status-not-started { background: linear-gradient(135deg, #f0f0f0, #e6e6e6); color: #555; } +.status-preparing, .status-initializing { background: linear-gradient(135deg, #fff7e0, #ffe8a3); color: #8a6d00; } +.status-running { background: linear-gradient(135deg, #e3f2fd, #bbdefb); color: #0d47a1; } +.status-done { background: linear-gradient(135deg, #e8f5e9, #c8e6c9); color: #1b5e20; } +.status-error { background: linear-gradient(135deg, #ffebee, #ffcdd2); color: #b71c1c; } diff --git a/monitor/templates/index.html b/monitor/templates/index.html new file mode 100644 index 0000000..ceffbea --- /dev/null +++ b/monitor/templates/index.html @@ -0,0 +1,41 @@ + + + + + + OSWorld Monitor + + + + + +
+

OSWorld Monitor System Online

+
+
+ + Loading... +
Total Tasks
+
+
+ + Loading... +
Active
+
+
+ + Loading... +
Completed
+
+
+
+
+
+
Loading task data...
+
+
+
+ + + + \ No newline at end of file diff --git a/monitor/templates/task_detail.html b/monitor/templates/task_detail.html new file mode 100644 index 0000000..fd5fb8f --- /dev/null +++ b/monitor/templates/task_detail.html @@ -0,0 +1,65 @@ + + + + + + Task Detail: {{ task_id }} + + + + +
+ Back to Home +

Task Detail {{ task_id }}

+
+

Basic Information

+
+
Task ID
+
{{ task_id }}
+
Task Type
+
{{ task_type }}
+
Instruction
+
{{ task_info.instruction }}
+
Status
+
{{ task_status.status }}
+
Current Step
+
{{ task_status.progress }}
+
Last Update
+
{{ task_status.last_update or 'None' }}
+
Result
+
{{ task_status.result }}
+
+
+
+

Execution Steps

+
+ {% if task_status.steps %} + {% for step in task_status.steps %} +
+
+
Step {{ step.step_num }}
+
{{ step.action_timestamp }}
+
+
{{ step.action.action }}
+ {% if step.screenshot_file %} +
+ Step {{ step.step_num }} Screenshot +
+ {% endif %} +
+ {% endfor %} + {% else %} +
No step data available
+ {% endif %} +
+
+
+ + + + \ No newline at end of file