PageSourceSearch

https://tyga.ai/js/ui/aiManagement/panels/overviewPanel.js

js tyga.ai collected 2026-10-05 15:11:31 UTC 14,531 bytes, 448 lines download raw bytes

1/**
2 * Overview Panel - System metrics and status display
3 * Shows active agents, success rate, costs, and recent activity
4 */
5
6import { api } from '../../../common/api.js';
7import { MetricWidget } from '../../../agents/components/index.js';
8import PollingManager from '../../../agents/pollingManager.js';
9import ActivityMonitor from '../../../agents/activityMonitor.js';
10
11class OverviewPanel {
12  constructor(dashboard) {
13    this.dashboard = dashboard;
14    this.pollingManager = PollingManager.getInstance();
15    this.activityMonitor = ActivityMonitor.getInstance();
16    this.unsubscribers = [];
17  }
18
19  async render(container) {
20    container.innerHTML = `
21      <div class="overview-panel">
22        <div class="d-flex justify-content-between align-items-center mb-4">
23          <h3>AI System Overview</h3>
24          <div>
25            <span class="badge bg-secondary me-2" id="polling-state">Loading...</span>
26            <button class="btn btn-sm btn-outline-primary" id="refreshMetrics">
27              <i class="bi bi-arrow-clockwise"></i> Refresh
28            </button>
29          </div>
30        </div>
31        
32        <div class="metrics-grid mb-4" id="metrics-grid">
33          <div class="text-center text-muted">Loading metrics...</div>
34        </div>
35        
36        <div class="row">
37          <div class="col-md-6">
38            <div class="card">
39              <div class="card-header">
40                <h5 class="mb-0">Recent Activity</h5>
41              </div>
42              <div class="card-body" id="recent-activity">
43                <div class="text-center text-muted">Loading activity...</div>
44              </div>
45            </div>
46          </div>
47          
48          <div class="col-md-6">
49            <div class="card">
50              <div class="card-header">
51                <h5 class="mb-0">System Health</h5>
52              </div>
53              <div class="card-body" id="system-health">
54                <div class="text-center text-muted">Loading health...</div>
55              </div>
56            </div>
57          </div>
58        </div>
59      </div>
60    `;
61    
62    this.setupEventListeners();
63    this.setupPolling();
64    
65    // Start activity monitor
66    this.activityMonitor.start();
67    
68    // Update polling state display
69    this.updatePollingStateDisplay();
70  }
71
72  setupEventListeners() {
73    const refreshBtn = document.getElementById('refreshMetrics');
74    if (refreshBtn) {
75      refreshBtn.addEventListener('click', () => {
76        this.pollingManager.refresh('/api/agents/metrics');
77        this.pollingManager.refresh('/api/agents/tasks?limit=5');
78      });
79    }
80  }
81
82  setupPolling() {
83    // Subscribe to metrics endpoint
84    const metricsUnsub = this.pollingManager.subscribe(
85      '/api/agents/metrics',
86      (response) => this.handleMetricsUpdate(response)
87    );
88    this.unsubscribers.push(metricsUnsub);
89
90    // Subscribe to tasks endpoint
91    const tasksUnsub = this.pollingManager.subscribe(
92      '/api/agents/tasks?limit=5',
93      (response) => this.handleTasksUpdate(response)
94    );
95    this.unsubscribers.push(tasksUnsub);
96
97    // Listen for activity state changes
98    const activityUnsub = this.activityMonitor.onChange((newState) => {
99      console.log('Activity state changed:', newState);
100      this.pollingManager.setActivityState(newState);
101      this.updatePollingStateDisplay();
102    });
103    this.unsubscribers.push(activityUnsub);
104  }
105
106  handleMetricsUpdate(response) {
107    // Handle errors and rate limiting
108    if (response.error || response.rateLimited) {
109      if (response.rateLimited) {
110        this.showRateLimitWarning(response.backoffDelay);
111      }
112      return;
113    }
114
115    const metrics = response.data || this.getDefaultMetrics();
116    this.renderMetrics(metrics);
117    this.renderHealth(metrics);
118    
119    // Feed data to ActivityMonitor (prevents direct API calls)
120    this.activityMonitor.updateActivityData({
121      activeAgents: metrics.activeAgents || 0
122    });
123  }
124
125  handleTasksUpdate(response) {
126    // Handle errors and rate limiting
127    if (response.error || response.rateLimited) {
128      return;
129    }
130
131    const tasks = response.data?.tasks || [];
132    this.renderActivity(tasks);
133    
134    // Feed data to ActivityMonitor (prevents direct API calls)
135    const recentTasks = tasks.filter(task => 
136      task.status === 'running' || task.status === 'pending'
137    ).length;
138    this.activityMonitor.updateActivityData({
139      recentTasks
140    });
141  }
142
143  showRateLimitWarning(backoffDelay) {
144    const grid = document.getElementById('metrics-grid');
145    if (grid && !grid.querySelector('.rate-limit-warning')) {
146      const warning = document.createElement('div');
147      warning.className = 'alert alert-warning rate-limit-warning';
148      warning.innerHTML = `
149        <i class="bi bi-exclamation-triangle"></i>
150        Rate limited. Backing off for ${Math.round(backoffDelay / 1000)}s...
151      `;
152      grid.prepend(warning);
153      
154      // Remove after backoff period
155      setTimeout(() => warning.remove(), backoffDelay);
156    }
157  }
158
159  updatePollingStateDisplay() {
160    const badge = document.getElementById('polling-state');
161    if (!badge) return;
162
163    const state = this.activityMonitor.getState();
164    const metrics = this.pollingManager.getMetrics();
165    
166    badge.className = 'badge me-2';
167    badge.className += state === 'IDLE' ? ' bg-secondary' :
168                       state === 'ACTIVE' ? ' bg-primary' :
169                       state === 'PEAK' ? ' bg-warning' : ' bg-secondary';
170    
171    badge.textContent = `${state} (Cache: ${Math.round(metrics.cacheHitRate * 100)}%)`;
172    badge.title = `Polling: ${state}\nCache Hit Rate: ${Math.round(metrics.cacheHitRate * 100)}%\nActive Endpoints: ${metrics.activeEndpoints}`;
173  }
174
175  getDefaultMetrics() {
176    return {
177      activeAgents: 0,
178      totalAgents: 0,
179      completedTasks: 0,
180      failedTasks: 0,
181      successRate: 0,
182      dailyCost: 0,
183      monthlyCost: 0,
184      avgTaskDuration: 0
185    };
186  }
187
188  renderMetrics(metrics) {
189    const grid = document.getElementById('metrics-grid');
190    if (!grid) return;
191    
192    grid.innerHTML = '';
193    
194    // Show "Coming Soon" message if no real data
195    const hasData = metrics.totalAgents > 0 || metrics.completedTasks > 0;
196    if (!hasData) {
197      grid.innerHTML = `
198        <div class="alert alert-info border-0" style="grid-column: 1 / -1;">
199          <div class="d-flex align-items-center">
200            <i class="bi bi-info-circle me-2" style="font-size: 1.5rem;"></i>
201            <div>
202              <h6 class="mb-1">Parallel AI Monitoring Coming Soon</h6>
203              <small class="text-muted">
204                Metrics will appear here when parallel AI tasks are running. 
205                Send complex requests to see agents in action!
206              </small>
207            </div>
208          </div>
209        </div>
210      `;
211      return;
212    }
213    
214    const widgets = [
215      { 
216        icon: 'bi-lightning-charge', 
217        label: 'Active Agents', 
218        value: metrics.activeAgents || 0, 
219        color: 'primary',
220        subtitle: `${metrics.totalAgents || 0} total`
221      },
222      { 
223        icon: 'bi-check-circle', 
224        label: 'Success Rate', 
225        value: `${((metrics.successRate || 0) * 100).toFixed(1)}%`, 
226        color: metrics.successRate >= 0.9 ? 'success' : metrics.successRate >= 0.7 ? 'warning' : 'danger',
227        subtitle: `${metrics.completedTasks || 0} completed`
228      },
229      { 
230        icon: 'bi-cash', 
231        label: 'Today\'s Cost', 
232        value: `$${(metrics.dailyCost || 0).toFixed(2)}`, 
233        color: 'warning',
234        subtitle: `$${(metrics.monthlyCost || 0).toFixed(2)} this month`
235      },
236      { 
237        icon: 'bi-clock', 
238        label: 'Avg Duration', 
239        value: this.formatDuration(metrics.avgTaskDuration || 0), 
240        color: 'info',
241        subtitle: `${metrics.completedTasks || 0} tasks`
242      }
243    ];
244    
245    widgets.forEach(config => {
246      const widget = new MetricWidget(config);
247      widget.render(grid);
248    });
249  }
250
251  renderActivity(tasks) {
252    const container = document.getElementById('recent-activity');
253    if (!container) return;
254    
255    if (tasks.length === 0) {
256      container.innerHTML = `
257        <div class="text-center py-4">
258          <i class="bi bi-hourglass text-muted" style="font-size: 2rem;"></i>
259          <p class="text-muted mb-0 mt-2">No tasks yet</p>
260          <small class="text-muted">Activity will appear when AI tasks run</small>
261        </div>
262      `;
263      return;
264    }
265    
266    container.innerHTML = `
267      <div class="activity-list">
268        ${tasks.map(task => `
269          <div class="activity-item">
270            <div class="d-flex justify-content-between align-items-start mb-1">
271              <div>
272                <strong>${this.escapeHtml(task.mode || 'Unknown')}</strong>
273                <span class="badge bg-${this.getStatusColor(task.status)} ms-2">
274                  ${this.escapeHtml(task.status)}
275                </span>
276              </div>
277              <small class="text-muted">${this.formatTime(task.createdAt)}</small>
278            </div>
279            <div class="text-muted small">
280              ${this.escapeHtml(task.description || 'No description')}
281            </div>
282            ${task.agents ? `
283              <div class="mt-1">
284                <small class="text-muted">${task.agents.length} agents</small>
285              </div>
286            ` : ''}
287          </div>
288        `).join('')}
289      </div>
290    `;
291  }
292
293  renderHealth(metrics) {
294    const container = document.getElementById('system-health');
295    if (!container) return;
296    
297    const health = this.calculateHealth(metrics);
298    
299    container.innerHTML = `
300      <div class="health-indicators">
301        <div class="health-item">
302          <div class="d-flex justify-content-between align-items-center mb-2">
303            <span>Overall Health</span>
304            <span class="badge bg-${health.overall.color}">${health.overall.label}</span>
305          </div>
306          <div class="progress" style="height: 8px;">
307            <div class="progress-bar bg-${health.overall.color}" 
308                 style="width: ${health.overall.score}%"></div>
309          </div>
310        </div>
311        
312        <div class="health-item mt-3">
313          <div class="d-flex justify-content-between mb-2">
314            <span class="small">Success Rate</span>
315            <span class="small">${((metrics.successRate || 0) * 100).toFixed(0)}%</span>
316          </div>
317          <div class="progress" style="height: 6px;">
318            <div class="progress-bar bg-${health.successRate.color}" 
319                 style="width: ${(metrics.successRate || 0) * 100}%"></div>
320          </div>
321        </div>
322        
323        <div class="health-item mt-3">
324          <div class="d-flex justify-content-between mb-2">
325            <span class="small">Active Load</span>
326            <span class="small">${metrics.activeAgents || 0}/20</span>
327          </div>
328          <div class="progress" style="height: 6px;">
329            <div class="progress-bar bg-${health.load.color}" 
330                 style="width: ${((metrics.activeAgents || 0) / 20) * 100}%"></div>
331          </div>
332        </div>
333        
334        <div class="mt-3">
335          <small class="text-muted">
336            <i class="bi bi-info-circle"></i>
337            ${health.message}
338          </small>
339        </div>
340      </div>
341    `;
342  }
343
344  calculateHealth(metrics) {
345    const successRate = metrics.successRate || 0;
346    const activeLoad = (metrics.activeAgents || 0) / 20;
347    
348    let overallScore = (successRate * 60) + ((1 - activeLoad) * 40);
349    overallScore = Math.min(100, Math.max(0, overallScore * 100));
350    
351    let overall = { score: overallScore };
352    if (overallScore >= 80) {
353      overall = { ...overall, color: 'success', label: 'Excellent' };
354    } else if (overallScore >= 60) {
355      overall = { ...overall, color: 'info', label: 'Good' };
356    } else if (overallScore >= 40) {
357      overall = { ...overall, color: 'warning', label: 'Fair' };
358    } else {
359      overall = { ...overall, color: 'danger', label: 'Poor' };
360    }
361    
362    const successRateHealth = {
363      color: successRate >= 0.9 ? 'success' : successRate >= 0.7 ? 'warning' : 'danger'
364    };
365    
366    const loadHealth = {
367      color: activeLoad <= 0.7 ? 'success' : activeLoad <= 0.9 ? 'warning' : 'danger'
368    };
369    
370    let message = 'System operating normally';
371    if (successRate < 0.7) {
372      message = 'Success rate below target. Review failed tasks.';
373    } else if (activeLoad > 0.9) {
374      message = 'High system load. Consider increasing capacity.';
375    }
376    
377    return {
378      overall,
379      successRate: successRateHealth,
380      load: loadHealth,
381      message
382    };
383  }
384
385  renderError() {
386    const grid = document.getElementById('metrics-grid');
387    if (grid) {
388      grid.innerHTML = `
389        <div class="alert alert-info border-0" style="grid-column: 1 / -1;">
390          <div class="d-flex align-items-center">
391            <i class="bi bi-info-circle me-2" style="font-size: 1.5rem;"></i>
392            <div>
393              <h6 class="mb-1">Dashboard Ready</h6>
394              <small class="text-muted">
395                Waiting for parallel AI tasks to run. The monitoring system will activate automatically 
396                when you send complex requests that require multiple agents.
397              </small>
398            </div>
399          </div>
400        </div>
401      `;
402    }
403  }
404
405  formatDuration(seconds) {
406    if (seconds < 60) return `${seconds.toFixed(0)}s`;
407    const minutes = Math.floor(seconds / 60);
408    const secs = Math.floor(seconds % 60);
409    return `${minutes}m ${secs}s`;
410  }
411
412  formatTime(timestamp) {
413    if (!timestamp) return 'Unknown';
414    const date = new Date(timestamp);
415    const now = new Date();
416    const diff = (now - date) / 1000; // seconds
417    
418    if (diff < 60) return 'Just now';
419    if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
420    if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
421    return date.toLocaleDateString();
422  }
423
424  getStatusColor(status) {
425    const colors = {
426      completed: 'success',
427      failed: 'danger',
428      running: 'primary',
429      pending: 'secondary',
430      cancelled: 'warning'
431    };
432    return colors[status] || 'secondary';
433  }
434
435  escapeHtml(text) {
436    const div = document.createElement('div');
437    div.textContent = text;
438    return div.innerHTML;
439  }
440
441  destroy() {
442    // Unsubscribe from all polling
443    this.unsubscribers.forEach(unsub => unsub());
444    this.unsubscribers = [];
445  }
446}
447
448export default OverviewPanel;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.