/** * AutoGLM Dashboard - Vue.js Application */ const { createApp } = Vue; createApp({ data() { return { devices: [], tasks: [], ws: null, wsConnected: false, refreshing: false, toasts: [], toastIdCounter: 0, reconnectAttempts: 0, maxReconnectAttempts: 5, reconnectDelay: 2000, }; }, computed: { activeTasks() { return this.tasks.filter(t => t.status === 'running'); }, connectedDeviceCount() { return this.devices.filter(d => d.is_connected).length; }, }, methods: { /* API Methods */ async loadDevices() { try { const response = await axios.get('/api/devices'); this.devices = response.data.map(d => ({ ...d, taskInput: '', screenshot: null, })); } catch (error) { this.showToast('Failed to load devices', 'error'); console.error('Error loading devices:', error); } }, async loadTasks() { try { const response = await axios.get('/api/tasks'); this.tasks = response.data; } catch (error) { console.error('Error loading tasks:', error); } }, async refreshDevices() { this.refreshing = true; try { await axios.get('/api/devices/refresh'); await this.loadDevices(); this.showToast('Devices refreshed', 'success'); } catch (error) { this.showToast('Failed to refresh devices', 'error'); } finally { this.refreshing = false; } }, async executeTask(device) { if (!device.taskInput || !device.taskInput.trim()) { return; } const task = device.taskInput.trim(); device.taskInput = ''; try { const response = await axios.post('/api/tasks/execute', { device_id: device.device_id, task: task, max_steps: 100, lang: 'cn', // Backend will use config from environment }); this.showToast(`Task started: ${task}`, 'success'); await this.loadTasks(); } catch (error) { this.showToast('Failed to start task', 'error'); console.error('Error executing task:', error); } }, async stopTask(task) { try { await axios.post(`/api/tasks/${task.task_id}/stop`); this.showToast('Stopping task...', 'info'); } catch (error) { this.showToast('Failed to stop task', 'error'); } }, async reExecuteTask(task) { const device = this.devices.find(d => d.device_id === task.device_id); if (!device) { this.showToast('Device not found', 'error'); return; } if (!device.is_connected) { this.showToast('Device is not connected', 'error'); return; } if (device.status === 'busy') { this.showToast('Device is busy', 'error'); return; } try { const response = await axios.post('/api/tasks/execute', { device_id: task.device_id, task: task.task, max_steps: task.max_steps || 100, lang: 'cn', }); this.showToast(`Task re-executed: ${task.task}`, 'success'); await this.loadTasks(); } catch (error) { this.showToast('Failed to re-execute task', 'error'); console.error('Error re-executing task:', error); } }, async captureScreenshot(deviceId) { try { const response = await axios.get(`/api/devices/${deviceId}/screenshot`); const device = this.devices.find(d => d.device_id === deviceId); if (device) { device.screenshot = response.data.screenshot; } } catch (error) { console.error('Error capturing screenshot:', error); } }, /* WebSocket Methods */ connectWebSocket() { const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${location.host}/ws`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { this.wsConnected = true; this.reconnectAttempts = 0; console.log('WebSocket connected'); // Subscribe to all devices updates this.sendWebSocketMessage({ type: 'subscribe', device_id: '*', // Subscribe to all devices }); }; this.ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); this.handleWSMessage(msg); } catch (error) { console.error('Error parsing WebSocket message:', error); } }; this.ws.onclose = () => { this.wsConnected = false; console.log('WebSocket disconnected'); // Attempt to reconnect if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; console.log(`Reconnecting... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); setTimeout(() => this.connectWebSocket(), this.reconnectDelay); } else { this.showToast('WebSocket connection lost', 'error'); } }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; }, sendWebSocketMessage(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } }, handleWSMessage(msg) { switch (msg.type) { case 'device_update': this.updateDevice(msg.data); break; case 'task_started': this.handleTaskStarted(msg.data); break; case 'task_step': this.handleTaskStep(msg.data); break; case 'task_completed': this.handleTaskCompleted(msg.data); break; case 'task_failed': this.handleTaskFailed(msg.data); break; case 'task_stopped': this.handleTaskStopped(msg.data); break; case 'screenshot': this.handleScreenshot(msg.data); break; case 'error': this.showToast(msg.data.error || 'An error occurred', 'error'); break; case 'pong': // Ping response, ignore break; default: console.log('Unknown WebSocket message type:', msg.type); } }, /* Message Handlers */ updateDevice(data) { const device = this.devices.find(d => d.device_id === data.device_id); if (device) { Object.assign(device, { status: data.status, is_connected: data.is_connected, model: data.model, current_app: data.current_app, }); } }, handleTaskStarted(data) { this.tasks.unshift({ task_id: data.task_id, device_id: data.device_id, task: data.task, status: 'running', current_step: 0, max_steps: data.max_steps || 100, current_action: null, thinking: null, started_at: new Date(), }); // Update device status const device = this.devices.find(d => d.device_id === data.device_id); if (device) { device.status = 'busy'; } }, handleTaskStep(data) { const task = this.tasks.find(t => t.task_id === data.task_id); if (task) { task.current_step = data.step; task.current_action = data.action; task.thinking = data.thinking; // Store the completion message if (data.message && data.finished) { task.completion_message = data.message; } if (data.finished) { task.status = data.success ? 'completed' : 'failed'; this.releaseDevice(data.device_id); } } }, handleTaskCompleted(data) { const task = this.tasks.find(t => t.task_id === data.task_id); if (task) { task.status = 'completed'; task.finished_at = new Date(); // Store completion message if (data.message) { task.completion_message = data.message; } this.releaseDevice(data.device_id); this.showToast(`Task completed: ${task.task}`, 'success'); } }, handleTaskFailed(data) { const task = this.tasks.find(t => t.task_id === data.task_id); if (task) { task.status = 'failed'; task.error = data.error; task.finished_at = new Date(); this.releaseDevice(data.device_id); this.showToast(`Task failed: ${data.error}`, 'error'); } }, handleTaskStopped(data) { const task = this.tasks.find(t => t.task_id === data.task_id); if (task) { task.status = 'stopped'; task.finished_at = new Date(); this.releaseDevice(data.device_id); this.showToast('Task stopped', 'info'); } }, handleScreenshot(data) { const device = this.devices.find(d => d.device_id === data.device_id); if (device) { device.screenshot = data.screenshot; } }, releaseDevice(deviceId) { const device = this.devices.find(d => d.device_id === deviceId); if (device && device.status === 'busy') { device.status = 'online'; } }, /* Utility Methods */ formatAppName(packageName) { if (!packageName) return ''; // Format package name for display const parts = packageName.split('.'); return parts[parts.length - 1] || packageName; }, showToast(message, type = 'info') { const id = this.toastIdCounter++; this.toasts.push({ id, message, type }); // Auto-remove after 3 seconds setTimeout(() => { this.toasts = this.toasts.filter(t => t.id !== id); }, 3000); }, /* Heartbeat */ startHeartbeat() { setInterval(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.sendWebSocketMessage({ type: 'ping' }); } }, 30000); // Ping every 30 seconds }, }, mounted() { // Load initial data this.loadDevices(); this.loadTasks(); // Connect WebSocket this.connectWebSocket(); // Start heartbeat this.startHeartbeat(); // Refresh devices periodically setInterval(() => { if (!this.refreshing) { this.loadDevices(); } }, 10000); // Every 10 seconds // Refresh tasks periodically setInterval(() => { this.loadTasks(); }, 5000); // Every 5 seconds }, beforeUnmount() { // Close WebSocket on unmount if (this.ws) { this.ws.close(); } }, }).mount('#app');