agent_tracker/templates/agent_management.html
nickviljoen 3bc757b99c Add star rating display on agent cards and rating filter
Show inline star icons on agent listing cards so ratings are visible
without opening the detail modal. Add rating filter dropdown (5, 4+,
3+, 2+, 1+, Unrated) alongside existing filters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:16:49 +02:00

1646 lines
No EOL
67 KiB
HTML

{% extends "base.html" %}
{% block title %}Agent Management - AgentHub{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2><i class="fas {{ 'fa-globe' if current_view == 'all' else 'fa-robot' }} me-3"></i>{{ page_title }}</h2>
<p class="text-muted mb-0">{{ page_description }}</p>
</div>
<div class="d-flex gap-2">
<a href="{{ base_path }}/agent-register" class="btn btn-success">
<i class="fas fa-plus me-2"></i>Register New Agent
</a>
<a href="{{ base_path }}/search" class="btn btn-outline-primary">
<i class="fas fa-search me-2"></i>Search
</a>
</div>
</div>
<!-- Success/Error Messages -->
{% if success %}
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i>{{ success }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
</div>
</div>
{% endif %}
{% if error %}
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>{{ error }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
</div>
</div>
{% endif %}
<!-- View Toggle Tabs -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body py-3">
<div class="d-flex justify-content-center">
<div class="btn-group" role="group" aria-label="Agent view toggle">
<input type="radio" class="btn-check" name="viewToggle" id="allAgentsTab" autocomplete="off" {% if current_view == 'all' %}checked{% endif %}>
<label class="btn btn-outline-primary" for="allAgentsTab">
<i class="fas fa-globe me-2"></i>All Agents
<span class="badge bg-primary ms-2" id="allAgentsCount">{{ agent_count if current_view == 'all' else '0' }}</span>
</label>
<input type="radio" class="btn-check" name="viewToggle" id="myAgentsTab" autocomplete="off" {% if current_view == 'my' %}checked{% endif %}>
<label class="btn btn-outline-primary" for="myAgentsTab">
<i class="fas fa-robot me-2"></i>My Agents
<span class="badge bg-primary ms-2" id="myAgentsCount">{{ agent_count if current_view == 'my' else '0' }}</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters and Search -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-2 mb-3 mb-md-0">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="searchInput" placeholder="Search agents...">
</div>
</div>
<div class="col-md-2 mb-3 mb-md-0">
<select class="form-select" id="statusFilter">
<option value="">All Statuses</option>
<option value="Active">Active</option>
<option value="Development">Development</option>
<option value="Inactive">Inactive</option>
<option value="Deprecated">Deprecated</option>
</select>
</div>
<div class="col-md-2 mb-3 mb-md-0">
<select class="form-select" id="auditFilter">
<option value="">All Audit</option>
<option value="audited">Audited</option>
<option value="not_audited">Not Audited</option>
</select>
</div>
<div class="col-md-2 mb-3 mb-md-0">
<select class="form-select" id="disciplineFilter">
<option value="">All Disciplines</option>
<option value="Strategy">Strategy</option>
<option value="Creative">Creative</option>
<option value="Oversight including delivery">Oversight including delivery</option>
<option value="Optimization">Optimization</option>
<option value="Back Office including operations">Back Office including operations</option>
<option value="Pencil Agents">Pencil Agents</option>
</select>
</div>
<div class="col-md-2 mb-3 mb-md-0">
<select class="form-select" id="ratingFilter">
<option value="">All Ratings</option>
<option value="5">5 Stars</option>
<option value="4">4+ Stars</option>
<option value="3">3+ Stars</option>
<option value="2">2+ Stars</option>
<option value="1">1+ Stars</option>
<option value="unrated">Unrated</option>
</select>
</div>
<div class="col-md-2 mb-3 mb-md-0">
<select class="form-select" id="sortBy">
<option value="created_at">Sort by Created Date</option>
<option value="name">Sort by Name</option>
<option value="status">Sort by Status</option>
<option value="rating">Sort by Rating</option>
<option value="total_messages">Sort by Total Messages</option>
<option value="total_tokens">Sort by Total Tokens</option>
<option value="unique_users">Sort by Unique Users</option>
</select>
</div>
<div class="col-auto">
<button class="btn btn-outline-primary" id="refreshBtn">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Agents List -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0" id="agentListTitle">{{ 'All AI Agents' if current_view == 'all' else 'Your AI Agents' }}</h5>
<span class="badge bg-primary" id="agentCount">{{ agent_count }}</span>
</div>
</div>
<div class="card-body p-0">
<div id="agentsContainer">
{% if agents %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th>Agent Name</th>
<th>Status</th>
<th>Version</th>
<th>Department</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for agent in agents %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="agent-avatar me-3">
<i class="fas fa-robot"></i>
</div>
<div>
<h6 class="mb-0">{{ agent.agent_name }}</h6>
<small class="text-muted">{{ agent.agent_description[:50] }}{% if agent.agent_description|length > 50 %}...{% endif %}</small>
</div>
</div>
</td>
<td>
{% set status_class = {'Active': 'success', 'Development': 'warning', 'Inactive': 'secondary', 'Deprecated': 'danger'} %}
<span class="badge bg-{{ status_class.get(agent.agent_status, 'secondary') }}">
{{ agent.agent_status or 'Unknown' }}
</span>
</td>
<td>{{ agent.agent_version or 'N/A' }}</td>
<td>{{ agent.agent_department or 'N/A' }}</td>
<td>{{ agent.created_at.strftime('%Y-%m-%d') if agent.created_at else 'N/A' }}</td>
<td>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-info" onclick="viewAgent('{{ agent._id|string }}')">
<i class="fas fa-eye"></i>
</button>
{% set can_edit = agent.created_by == current_user._id|string or (current_user.is_admin) or (agent.agent_contact_person and current_user.email and agent.agent_contact_person.lower() == current_user.email.lower()) %}
{% if can_edit %}
<button type="button" class="btn btn-sm btn-outline-warning" onclick="editAgent('{{ agent._id|string }}')">
<i class="fas fa-edit"></i>
</button>
{% else %}
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="You can only edit agents you own or are the contact person for">
<i class="fas fa-edit"></i>
</button>
{% endif %}
{% if agent.created_by == current_user._id|string %}
<button type="button" class="btn btn-sm btn-outline-danger" onclick="confirmDelete('{{ agent._id|string }}')">
<i class="fas fa-trash"></i>
</button>
{% else %}
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="You can only delete agents you own">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<div class="mb-3">
<i class="fas fa-robot fa-3x text-muted"></i>
</div>
<h5>No Agents Yet</h5>
<p class="text-muted">You haven't created any agents yet. Click the button below to get started!</p>
<a href="{{ base_path }}/agent-register" class="btn btn-success">
<i class="fas fa-plus me-2"></i>Create Your First Agent
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Agent Details Modal -->
<div class="modal fade" id="agentModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Agent Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="modalContent"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<div id="modalActionButtons"></div>
</div>
</div>
</div>
</div>
<!-- Edit Agent Modal -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Agent</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editAgentForm">
<input type="hidden" id="editAgentId">
<div class="row">
<div class="col-md-6 mb-3">
<label for="editAgentName" class="form-label">Agent Name *</label>
<input type="text" class="form-control" id="editAgentName" required>
</div>
<div class="col-md-6 mb-3">
<label for="editAgentTool" class="form-label">Tool *</label>
<input type="text" class="form-control" id="editAgentTool" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editAgentStatus" class="form-label">Status</label>
<select class="form-select" id="editAgentStatus">
<option value="Development">Development</option>
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
<option value="Deprecated">Deprecated</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="editAgentVersion" class="form-label">Version</label>
<input type="text" class="form-control" id="editAgentVersion">
</div>
</div>
<div class="mb-3">
<label for="editAgentDescription" class="form-label">Description</label>
<textarea class="form-control" id="editAgentDescription" rows="3" maxlength="300"></textarea>
</div>
<div class="mb-3">
<label for="editAgentPurpose" class="form-label">Purpose</label>
<input type="text" class="form-control" id="editAgentPurpose" maxlength="200">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editAgentLocation" class="form-label">Location</label>
<input type="text" class="form-control" id="editAgentLocation">
</div>
<div class="col-md-6 mb-3">
<label for="editAgentDepartment" class="form-label">Department</label>
<input type="text" class="form-control" id="editAgentDepartment">
</div>
</div>
<div class="mb-3">
<label for="editAgentContact" class="form-label">Contact Person</label>
<input type="text" class="form-control" id="editAgentContact">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editAgentTags" class="form-label">Tags</label>
<input type="text" class="form-control" id="editAgentTags" placeholder="Separate with commas">
</div>
<div class="col-md-6 mb-3">
<label for="editAgentUserbase" class="form-label">Target Userbase</label>
<input type="text" class="form-control" id="editAgentUserbase" placeholder="Separate with commas">
</div>
</div>
<div class="mb-3">
<label for="editAgentCapabilities" class="form-label">Capabilities</label>
<input type="text" class="form-control" id="editAgentCapabilities" placeholder="Separate with commas">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editAgentDiscipline" class="form-label">
<i class="fas fa-layer-group me-2"></i>Discipline
</label>
<select class="form-select" id="editAgentDiscipline">
<option value="">Select Discipline</option>
<option value="Strategy">Strategy</option>
<option value="Creative">Creative</option>
<option value="Oversight including delivery">Oversight including delivery</option>
<option value="Optimization">Optimization</option>
<option value="Back Office including operations">Back Office including operations</option>
<option value="Pencil Agents">Pencil Agents</option>
</select>
</div>
</div>
<div class="mb-3" id="editQualityAuditSection">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="editQualityAuditStatus" onchange="toggleEditRiskFactor()">
<label class="form-check-label" for="editQualityAuditStatus">
<i class="fas fa-certificate me-2"></i>Quality Audit
<span class="badge bg-warning text-dark ms-2">Admin Only</span>
</label>
</div>
<div class="form-text" id="editQualityAuditNote">
Only administrators can modify quality audit status.
</div>
</div>
<div class="mb-3" id="editRiskFactorSection" style="display: none;">
<label for="editRiskFactor" class="form-label">
<i class="fas fa-exclamation-triangle me-2"></i>Risk Factor
<span class="badge bg-warning text-dark ms-2">Admin Only</span>
<span class="text-danger">*</span>
</label>
<select class="form-select" id="editRiskFactor">
<option value="">Select Risk Level</option>
<option value="1">1 - Very Low Risk</option>
<option value="2">2 - Low Risk</option>
<option value="3">3 - Medium Risk</option>
<option value="4">4 - High Risk</option>
<option value="5">5 - Very High Risk</option>
</select>
<div class="form-text" id="editRiskFactorNote">
Required when Quality Audit is checked. Select the appropriate risk level for this agent.
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="editAgentForm" class="btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</div>
<!-- Rating Framework Info Modal -->
<div class="modal fade" id="ratingFrameworkModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-info-circle me-2"></i>Rating Framework</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<h6 class="mb-3">Star Rating Scale</h6>
<table class="table table-bordered table-sm">
<thead class="table-light">
<tr><th>Rating</th><th>Level</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><span class="text-warning">&#9733;</span> 1</td><td>Initial</td><td>Agent is in early stages; minimal functionality, limited testing, and not yet ready for regular use.</td></tr>
<tr><td><span class="text-warning">&#9733;&#9733;</span> 2</td><td>Developing</td><td>Agent has basic functionality but significant gaps remain; occasional errors and inconsistent outputs.</td></tr>
<tr><td><span class="text-warning">&#9733;&#9733;&#9733;</span> 3</td><td>Competent</td><td>Agent performs core tasks reliably; meets basic expectations with acceptable accuracy and response quality.</td></tr>
<tr><td><span class="text-warning">&#9733;&#9733;&#9733;&#9733;</span> 4</td><td>Proficient</td><td>Agent delivers high-quality results consistently; handles edge cases well and provides good user experience.</td></tr>
<tr><td><span class="text-warning">&#9733;&#9733;&#9733;&#9733;&#9733;</span> 5</td><td>Expert</td><td>Agent excels across all dimensions; exceptional accuracy, reliability, and user satisfaction.</td></tr>
</tbody>
</table>
<h6 class="mt-4 mb-3">Performance Areas</h6>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h6 class="card-title"><i class="fas fa-bullseye me-2 text-primary"></i>Accuracy & Reliability</h6>
<p class="card-text small text-muted mb-0">How correct and consistent are the agent's outputs? Does it handle inputs reliably without errors?</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h6 class="card-title"><i class="fas fa-user-check me-2 text-success"></i>User Experience</h6>
<p class="card-text small text-muted mb-0">Is the agent easy to interact with? Does it understand user intent and provide clear, helpful responses?</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h6 class="card-title"><i class="fas fa-tachometer-alt me-2 text-warning"></i>Efficiency & Performance</h6>
<p class="card-text small text-muted mb-0">Does the agent respond promptly? Does it complete tasks without unnecessary steps or resource usage?</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h6 class="card-title"><i class="fas fa-expand-arrows-alt me-2 text-info"></i>Scope & Adaptability</h6>
<p class="card-text small text-muted mb-0">How well does the agent handle varied or unexpected scenarios? Can it adapt to different user needs?</p>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<style>
.card-star-rating {
display: inline-flex;
align-items: center;
gap: 1px;
font-size: 0.9rem;
line-height: 1;
}
.agent-card {
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s ease;
cursor: pointer;
}
.agent-card:hover {
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.agent-status {
font-size: 0.8rem;
padding: 4px 8px;
border-radius: 20px;
}
.status-Active { background-color: #d4edda; color: #155724; }
.status-Development { background-color: #fff3cd; color: #856404; }
.status-Inactive { background-color: #f8d7da; color: #721c24; }
.status-Deprecated { background-color: #e2e3e5; color: #41464b; }
.bg-orange {
background-color: #fd7e14 !important;
color: white !important;
}
.agent-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
}
.agent-avatar {
width: 35px;
height: 35px;
border-radius: 8px;
background: linear-gradient(135deg, #f3ae3e 0%, #f3ae3e 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1rem;
}
.no-agents {
text-align: center;
padding: 60px 20px;
color: #6c757d;
}
.card-header {
border-radius: 15px 15px 0 0 !important;
}
</style>
<script>
let agents = [];
let allAgents = [];
let myAgents = [];
let currentAgentId = null;
let currentView = '{{ current_view }}';
let currentUserId = '{{ current_user._id|string }}';
let currentUserEmail = '{{ current_user.email }}';
let currentUserIsAdmin = {{ 'true' if current_user.is_admin else 'false' }};
const basePath = '{{ base_path }}';
// Helper function to check if current user can edit an agent
function canUserEditAgent(agent) {
// Admin can edit any agent
if (currentUserIsAdmin) {
return true;
}
// User can edit agents they created
if (agent.created_by === currentUserId) {
return true;
}
// User can edit agents where they are the contact person (case-insensitive)
if (agent.agent_contact_person && currentUserEmail) {
return agent.agent_contact_person.toLowerCase() === currentUserEmail.toLowerCase();
}
return false;
}
document.addEventListener('DOMContentLoaded', function() {
// Load both datasets initially for accurate counts
Promise.all([loadAllAgents(), loadMyAgents()]).then(() => {
// Display the current view
loadAgentsForCurrentView();
});
setupEventListeners();
});
function setupEventListeners() {
document.getElementById('searchInput').addEventListener('input', filterAgents);
document.getElementById('statusFilter').addEventListener('change', filterAgents);
document.getElementById('auditFilter').addEventListener('change', filterAgents);
document.getElementById('disciplineFilter').addEventListener('change', filterAgents);
document.getElementById('ratingFilter').addEventListener('change', filterAgents);
document.getElementById('sortBy').addEventListener('change', sortAndDisplayAgents);
document.getElementById('refreshBtn').addEventListener('click', function() {
// Reload both datasets to ensure counts are accurate
Promise.all([loadAllAgents(), loadMyAgents()]).then(() => {
// Display the current view
loadAgentsForCurrentView();
});
});
// Note: editAgentBtn and deleteAgentBtn event listeners are now added dynamically in showAgentDetails
document.getElementById('editAgentForm').addEventListener('submit', updateAgent);
// Tab switching event listeners
document.getElementById('allAgentsTab').addEventListener('change', function() {
if (this.checked) switchToView('all');
});
document.getElementById('myAgentsTab').addEventListener('change', function() {
if (this.checked) switchToView('my');
});
}
async function loadAgentsForCurrentView() {
if (currentView === 'all') {
agents = allAgents;
} else {
agents = myAgents;
}
displayAgents(agents);
updateAgentCounts();
}
async function loadAllAgents() {
try {
console.log('DEBUG: Loading all agents from server...');
const response = await fetch('{{ base_path }}/api/agents/all', {
credentials: 'include'
});
if (response.ok) {
allAgents = await response.json();
console.log('DEBUG: Loaded all agents:', allAgents.length, 'agents');
console.log('DEBUG: First agent Quality Audit data:', allAgents.length > 0 ? {
name: allAgents[0].agent_name,
quality_audit_status: allAgents[0].quality_audit_status,
quality_audit_updated_by_name: allAgents[0].quality_audit_updated_by_name
} : 'No agents');
agents = allAgents;
displayAgents(agents);
updateAgentCounts();
} else if (response.status === 401) {
window.location.href = '{{ base_path }}/login';
} else {
throw new Error('Failed to load all agents');
}
} catch (error) {
console.error('Error loading all agents:', error);
showError('Failed to load all agents');
}
}
async function loadMyAgents() {
try {
const response = await fetch('{{ base_path }}/api/agents', {
credentials: 'include'
});
if (response.ok) {
myAgents = await response.json();
agents = myAgents;
displayAgents(agents);
updateAgentCounts();
} else if (response.status === 401) {
window.location.href = '{{ base_path }}/login';
} else {
throw new Error('Failed to load my agents');
}
} catch (error) {
console.error('Error loading my agents:', error);
showError('Failed to load my agents');
}
}
function switchToView(view) {
if (view === currentView) return;
currentView = view;
// Update page title and description
const pageTitle = document.querySelector('h2 i');
const pageDescription = document.querySelector('.text-muted');
const agentListTitle = document.getElementById('agentListTitle');
if (view === 'all') {
pageTitle.className = 'fas fa-globe me-3';
pageTitle.nextSibling.textContent = 'All Agents';
agentListTitle.textContent = 'All AI Agents';
agents = allAgents;
if (allAgents.length === 0) {
loadAllAgents();
return;
}
} else {
pageTitle.className = 'fas fa-robot me-3';
pageTitle.nextSibling.textContent = 'My Agents Dashboard';
agentListTitle.textContent = 'Your AI Agents';
agents = myAgents;
if (myAgents.length === 0) {
loadMyAgents();
return;
}
}
displayAgents(agents);
updateAgentCounts();
// Update URL without page reload
const newUrl = view === 'my' ? `${basePath}/agent-management?view=my` : `${basePath}/agent-management`;
window.history.pushState({view: view}, '', newUrl);
}
function displayAgents(agentsToShow) {
const container = document.getElementById('agentsContainer');
if (agentsToShow.length === 0) {
container.innerHTML = `
<div class="no-agents">
<i class="fas fa-robot fa-3x mb-3"></i>
<h5>No agents found</h5>
<p class="mb-3">You haven't created any AI agents yet.</p>
<a href="{{ base_path }}/agent-register" class="btn btn-success">
<i class="fas fa-plus me-2"></i>Create Your First Agent
</a>
</div>
`;
return;
}
const agentsHtml = agentsToShow.map(agent => `
<div class="agent-card" onclick="showAgentDetails('${agent.agent_id}')">
<div class="row align-items-center">
<div class="col-auto">
<div class="agent-icon">
<i class="fas fa-robot"></i>
</div>
</div>
<div class="col">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1 fw-bold">${agent.agent_name}</h6>
<p class="text-muted mb-2 small">${agent.agent_description || 'No description'}</p>
<div class="d-flex gap-2 align-items-center flex-wrap mb-1">
<span class="agent-status status-${agent.agent_status || 'Development'}">
${agent.agent_status || 'Development'}
</span>
${agent.agent_version ? `<span class="badge bg-light text-dark">v${agent.agent_version}</span>` : ''}
${agent.discipline ? `<span class="badge bg-purple" title="Discipline"><i class="fas fa-layer-group me-1"></i>${agent.discipline}</span>` : ''}
${agent.rating ? `<span class="card-star-rating" title="Rating: ${agent.rating}/5">${getCardStars(agent.rating)} <small class="text-muted">${agent.rating.toFixed(1)}${agent.rating_count ? ' (' + agent.rating_count + ')' : ''}</small></span>` : ''}
${agent.quality_audit_status ? '<span class="badge bg-success" title="Quality Audited"><i class="fas fa-certificate"></i></span>' : ''}
${agent.quality_audit_status && agent.risk_factor ? getRiskFactorBadge(agent.risk_factor) : ''}
${agent.total_messages ? `<span class="badge bg-info text-white" title="Total Messages"><i class="fas fa-comments me-1"></i>${agent.total_messages.toLocaleString()}</span>` : ''}
${agent.total_tokens ? `<span class="badge bg-warning text-dark" title="Total Tokens${agent.prompt_tokens != null ? '\nPrompt: ' + agent.prompt_tokens.toLocaleString() : ''}${agent.completion_tokens != null ? '\nCompletion: ' + agent.completion_tokens.toLocaleString() : ''}"><i class="fas fa-coins me-1"></i>${agent.total_tokens.toLocaleString()}</span>` : ''}
${agent.unique_users ? `<span class="badge bg-primary" title="Unique Users"><i class="fas fa-users me-1"></i>${agent.unique_users}</span>` : ''}
${agent.url ? `<a href="${agent.url}" target="_blank" class="btn btn-sm btn-success" onclick="event.stopPropagation();" title="Start a conversation with this agent"><i class="fas fa-external-link-alt me-1"></i>Open Agent</a>` : ''}
</div>
${agent.agent_tags && agent.agent_tags.length > 0 ? `
<div class="d-flex gap-1 flex-wrap">
${agent.agent_tags.map(tag => `<span class="badge bg-secondary" style="font-size: 0.7rem;"><i class="fas fa-tag me-1"></i>${tag}</span>`).join('')}
</div>
` : ''}
</div>
<div class="text-end">
<small class="text-muted">${formatDate(agent.agent_created_at)}</small>
<div class="mt-1">
${canUserEditAgent(agent) ? `
<button class="btn btn-outline-primary btn-sm me-1" onclick="event.stopPropagation(); editAgent('${agent.agent_id}')">
<i class="fas fa-edit"></i>
</button>
` : `
<button class="btn btn-outline-secondary btn-sm" disabled title="You can only edit agents you own or are the contact person for">
<i class="fas fa-edit"></i>
</button>
`}
${agent.created_by === currentUserId ? `
<button class="btn btn-outline-danger btn-sm" onclick="event.stopPropagation(); confirmDelete('${agent.agent_id}')">
<i class="fas fa-trash"></i>
</button>
` : `
<button class="btn btn-outline-secondary btn-sm" disabled title="You can only delete agents you own">
<i class="fas fa-trash"></i>
</button>
`}
</div>
</div>
</div>
</div>
</div>
</div>
`).join('');
container.innerHTML = agentsHtml;
}
async function showAgentDetails(agentId) {
console.log('DEBUG: Fetching fresh agent details for ID:', agentId);
currentAgentId = agentId;
try {
// Fetch fresh agent data from server
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
credentials: 'include'
});
if (!response.ok) {
if (response.status === 401) {
window.location.href = '{{ base_path }}/login';
return;
} else if (response.status === 403) {
showError('Not authorized to view this agent');
return;
} else {
showError('Failed to load agent details');
return;
}
}
const agent = await response.json();
console.log('DEBUG: Fresh agent data loaded:', agent);
console.log('DEBUG: Agent URL field:', agent.url);
console.log('DEBUG: Quality Audit Details:', {
status: agent.quality_audit_status,
updated_by: agent.quality_audit_updated_by,
updated_by_name: agent.quality_audit_updated_by_name,
updated_at: agent.quality_audit_updated_at
});
const modalContent = `
<div class="row">
<div class="col-md-6">
<h6>Basic Information</h6>
<table class="table table-sm">
<tr><td><strong>Name:</strong></td><td>${agent.agent_name}</td></tr>
<tr><td><strong>Status:</strong></td><td><span class="agent-status status-${agent.agent_status || 'Development'}">${agent.agent_status || 'Development'}</span></td></tr>
<tr><td><strong>Version:</strong></td><td>${agent.agent_version || 'N/A'}</td></tr>
<tr><td><strong>Purpose:</strong></td><td>${agent.agent_purpose || 'N/A'}</td></tr>
${agent.url ? `<tr><td><strong>Agent Link:</strong></td><td><a href="${agent.url}" target="_blank" class="btn btn-sm btn-success"><i class="fas fa-external-link-alt me-1"></i>Open Agent</a></td></tr>` : ''}
<tr><td><strong>Discipline:</strong></td><td>${agent.discipline ? `<span class="badge bg-purple"><i class="fas fa-layer-group me-1"></i>${agent.discipline}</span>` : '<span class="text-muted">N/A</span>'}</td></tr>
<tr><td><strong>Rating:</strong> <a href="#" onclick="event.preventDefault(); new bootstrap.Modal(document.getElementById('ratingFrameworkModal')).show();" title="View rating framework"><i class="fas fa-info-circle text-primary"></i></a></td><td>
<span id="detailRatingDisplay" data-agent-id="${agent.agent_id}">${agent.rating ? getStarDisplay(agent.rating, agent.rating_count) : '<span class="text-muted">Not yet rated</span>'}</span>
</td></tr>
<tr><td><strong>Quality Audit:</strong></td><td>
${agent.quality_audit_status ?
'<span class="badge bg-success"><i class="fas fa-certificate me-1"></i>Audited</span>' :
'<span class="badge bg-secondary">Not Audited</span>'
}
${agent.quality_audit_status && agent.risk_factor ? getRiskFactorBadge(agent.risk_factor) : ''}
</td></tr>
</table>
</div>
<div class="col-md-6">
<h6>Details</h6>
<table class="table table-sm">
<tr><td><strong>Location:</strong></td><td>${agent.agent_location || 'N/A'}</td></tr>
<tr><td><strong>Department:</strong></td><td>${agent.agent_department || 'N/A'}</td></tr>
<tr><td><strong>Contact:</strong></td><td>${agent.agent_contact_person || 'N/A'}</td></tr>
<tr><td><strong>Created:</strong></td><td>${formatDate(agent.agent_created_at)}</td></tr>
<tr><td><strong>Last Edited By:</strong></td><td>${agent.last_edited_by || 'N/A'}</td></tr>
</table>
</div>
</div>
${agent.agent_description ? `
<div class="row mt-3">
<div class="col-12">
<h6>Description</h6>
<p class="text-muted">${agent.agent_description}</p>
</div>
</div>
` : ''}
${agent.agent_tags && agent.agent_tags.length > 0 ? `
<div class="row mt-3">
<div class="col-12">
<h6>Tags</h6>
<div>
${agent.agent_tags.map(tag => `<span class="badge bg-secondary me-1">${tag}</span>`).join('')}
</div>
</div>
</div>
` : ''}
${agent.agent_capabilities && agent.agent_capabilities.length > 0 ? `
<div class="row mt-3">
<div class="col-12">
<h6>Capabilities</h6>
<div>
${agent.agent_capabilities.map(cap => `<span class="badge bg-info me-1">${cap}</span>`).join('')}
</div>
</div>
</div>
` : ''}
${agent.agent_userbase && agent.agent_userbase.length > 0 ? `
<div class="row mt-3">
<div class="col-12">
<h6>Target Userbase</h6>
<div>
${agent.agent_userbase.map(user => `<span class="badge bg-success me-1">${user}</span>`).join('')}
</div>
</div>
</div>
` : ''}
<div class="row mt-3">
<div class="col-12">
<h6>Quality Audit History</h6>
${agent.quality_audit_updated_by_name && agent.quality_audit_updated_at ? `
<p class="text-muted small mb-0">
<i class="fas fa-user me-1"></i><strong>${agent.quality_audit_updated_by_name}</strong>
${agent.quality_audit_status ? 'checked' : 'unchecked'} Quality Audit on
<strong>${formatDate(agent.quality_audit_updated_at)}</strong>
</p>
` : `
<p class="text-muted small mb-0">
<i class="fas fa-info-circle me-1"></i>No quality audit changes recorded yet.
</p>
`}
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<h6>Usage Analytics</h6>
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="period" id="dailyPeriod" value="daily" checked>
<label class="btn btn-outline-primary btn-sm" for="dailyPeriod">Daily</label>
<input type="radio" class="btn-check" name="period" id="weeklyPeriod" value="weekly">
<label class="btn btn-outline-primary btn-sm" for="weeklyPeriod">Weekly</label>
<input type="radio" class="btn-check" name="period" id="monthlyPeriod" value="monthly">
<label class="btn btn-outline-primary btn-sm" for="monthlyPeriod">Monthly</label>
</div>
<div class="d-flex gap-2">
<input type="date" class="form-control form-control-sm" id="startDate" placeholder="Start date">
<input type="date" class="form-control form-control-sm" id="endDate" placeholder="End date">
<button class="btn btn-primary btn-sm" id="updateChart">Update</button>
</div>
</div>
<div id="usageStatsContainer">
<div class="text-center text-muted">
<div class="spinner-border spinner-border-sm" role="status"></div>
<p class="mt-2">Loading usage data...</p>
</div>
</div>
<canvas id="usageChart" style="display: none; max-height: 300px;"></canvas>
</div>
</div>
</div>
</div>
`;
document.getElementById('modalContent').innerHTML = modalContent;
// Create action buttons based on edit permissions
const canEdit = canUserEditAgent(agent);
const isOwner = agent.created_by === currentUserId;
const isAdmin = {{ 'true' if current_user.role == 'admin' else 'false' }};
let actionButtonsHtml = '';
if (canEdit) {
actionButtonsHtml += `<button type="button" class="btn btn-warning" id="editAgentBtn">Edit</button>`;
}
if (isOwner || isAdmin) {
actionButtonsHtml += `<button type="button" class="btn btn-danger" id="deleteAgentBtn">Delete</button>`;
}
document.getElementById('modalActionButtons').innerHTML = actionButtonsHtml;
// Add event listeners to the dynamically created buttons
if (canEdit && document.getElementById('editAgentBtn')) {
document.getElementById('editAgentBtn').addEventListener('click', showEditModal);
}
if ((isOwner || isAdmin) && document.getElementById('deleteAgentBtn')) {
document.getElementById('deleteAgentBtn').addEventListener('click', deleteAgent);
}
const modal = new bootstrap.Modal(document.getElementById('agentModal'));
modal.show();
// All authenticated users can rate - fetch user's own rating and show interactive stars
{
const ratingContainer = document.getElementById('detailRatingDisplay');
if (ratingContainer) {
try {
const ratingResp = await fetch(`{{ base_path }}/api/agents/${agent.agent_id}/my-rating`, { credentials: 'include' });
if (ratingResp.ok) {
const ratingData = await ratingResp.json();
const userRating = ratingData.user_rating || 0;
ratingContainer.innerHTML = getClickableStars(userRating, agent.agent_id);
// Show average below the stars
if (ratingData.rating) {
ratingContainer.innerHTML += ` <small class="text-muted">Avg: ${ratingData.rating.toFixed(1)} (${ratingData.rating_count} rating${ratingData.rating_count !== 1 ? 's' : ''})</small>`;
}
setupDetailStarListeners(agent.agent_id, userRating);
} else {
const currentRating = agent.rating || 0;
ratingContainer.innerHTML = getClickableStars(currentRating, agent.agent_id);
setupDetailStarListeners(agent.agent_id, currentRating);
}
} catch (err) {
const currentRating = agent.rating || 0;
ratingContainer.innerHTML = getClickableStars(currentRating, agent.agent_id);
setupDetailStarListeners(agent.agent_id, currentRating);
}
}
}
// Load usage data after modal is shown
loadUsageChart(agent.agent_name);
} catch (error) {
console.error('Error loading agent details:', error);
showError('Failed to load agent details. Please try again.');
}
}
async function showEditModal() {
console.log('showEditModal called with currentAgentId:', currentAgentId);
try {
// Fetch fresh agent data from server
const response = await fetch(`{{ base_path }}/api/agents/${currentAgentId}`, {
credentials: 'include'
});
if (!response.ok) {
if (response.status === 401) {
window.location.href = '{{ base_path }}/login';
return;
} else if (response.status === 403) {
showError('Not authorized to edit this agent');
return;
} else {
showError('Failed to load agent for editing');
return;
}
}
const agent = await response.json();
console.log('DEBUG: Fresh agent data for editing:', agent);
console.log('Agent Quality Audit Status:', agent.quality_audit_status);
console.log('Agent Quality Audit Updated By:', agent.quality_audit_updated_by_name);
console.log('Agent Quality Audit Updated At:', agent.quality_audit_updated_at);
console.log('Agent Quality Audit Updated By ID:', agent.quality_audit_updated_by);
// Populate edit form
document.getElementById('editAgentId').value = agent.agent_id;
document.getElementById('editAgentName').value = agent.agent_name;
document.getElementById('editAgentTool').value = agent.agent_tool || '';
document.getElementById('editAgentStatus').value = agent.agent_status || 'Development';
document.getElementById('editAgentDescription').value = agent.agent_description || '';
document.getElementById('editAgentPurpose').value = agent.agent_purpose || '';
document.getElementById('editAgentVersion').value = agent.agent_version || '';
document.getElementById('editAgentLocation').value = agent.agent_location || '';
document.getElementById('editAgentDepartment').value = agent.agent_department || '';
document.getElementById('editAgentContact').value = agent.agent_contact_person || '';
document.getElementById('editAgentTags').value = agent.agent_tags ? agent.agent_tags.join(', ') : '';
document.getElementById('editAgentUserbase').value = agent.agent_userbase ? agent.agent_userbase.join(', ') : '';
document.getElementById('editAgentCapabilities').value = agent.agent_capabilities ? agent.agent_capabilities.join(', ') : '';
document.getElementById('editAgentDiscipline').value = agent.discipline || '';
// Handle Quality Audit field
const qualityAuditCheckbox = document.getElementById('editQualityAuditStatus');
const qualityAuditSection = document.getElementById('editQualityAuditSection');
const riskFactorSection = document.getElementById('editRiskFactorSection');
const riskFactor = document.getElementById('editRiskFactor');
if (currentUserIsAdmin) {
qualityAuditSection.style.display = 'block';
qualityAuditCheckbox.checked = agent.quality_audit_status || false;
qualityAuditCheckbox.disabled = false;
// Update the note with audit trail information
let noteHtml = 'Check this box if the agent has passed quality audit review.';
if (agent.quality_audit_updated_by_name && agent.quality_audit_updated_at) {
const action = agent.quality_audit_status ? 'checked' : 'unchecked';
const date = formatDate(agent.quality_audit_updated_at);
noteHtml += `<br><br><small class="text-info"><i class="fas fa-history me-1"></i><strong>${agent.quality_audit_updated_by_name}</strong> ${action} Quality Audit on ${date}</small>`;
} else {
noteHtml += '<br><br><small class="text-muted"><i class="fas fa-info-circle me-1"></i>No quality audit changes recorded yet.</small>';
}
document.getElementById('editQualityAuditNote').innerHTML = noteHtml;
// Handle Risk Factor field
riskFactor.value = agent.risk_factor || '';
if (agent.quality_audit_status) {
riskFactorSection.style.display = 'block';
riskFactor.required = true;
} else {
riskFactorSection.style.display = 'none';
riskFactor.required = false;
}
} else {
qualityAuditSection.style.display = 'none';
riskFactorSection.style.display = 'none';
}
// Hide details modal if it's open, then show edit modal
const agentModal = bootstrap.Modal.getInstance(document.getElementById('agentModal'));
if (agentModal) {
agentModal.hide();
}
console.log('About to show edit modal');
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
editModal.show();
console.log('Edit modal show() called');
} catch (error) {
console.error('Error loading agent for editing:', error);
showError('Failed to load agent for editing. Please try again.');
}
}
async function updateAgent(e) {
e.preventDefault();
const agentId = document.getElementById('editAgentId').value;
const agentData = {
agent_name: document.getElementById('editAgentName').value,
agent_tool: document.getElementById('editAgentTool').value,
agent_status: document.getElementById('editAgentStatus').value,
agent_description: document.getElementById('editAgentDescription').value,
agent_purpose: document.getElementById('editAgentPurpose').value,
agent_version: document.getElementById('editAgentVersion').value,
agent_location: document.getElementById('editAgentLocation').value,
agent_department: document.getElementById('editAgentDepartment').value,
agent_contact_person: document.getElementById('editAgentContact').value,
agent_tags: document.getElementById('editAgentTags').value.split(',').map(s => s.trim()).filter(s => s),
agent_userbase: document.getElementById('editAgentUserbase').value.split(',').map(s => s.trim()).filter(s => s),
agent_capabilities: document.getElementById('editAgentCapabilities').value.split(',').map(s => s.trim()).filter(s => s),
discipline: document.getElementById('editAgentDiscipline').value || null
};
// Add Quality Audit status and Risk Factor if user is admin
if (currentUserIsAdmin) {
agentData.quality_audit_status = document.getElementById('editQualityAuditStatus').checked;
const riskFactorValue = document.getElementById('editRiskFactor').value;
agentData.risk_factor = riskFactorValue ? parseInt(riskFactorValue) : null;
}
// Validate the form before sending
if (!validateEditForm()) {
return;
}
console.log('Sending agent update data:', agentData);
try {
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(agentData)
});
if (response.ok) {
const updatedAgent = await response.json();
console.log('Update successful, received agent data:', updatedAgent);
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
// Properly reload data from server instead of using cached data
await Promise.all([loadAllAgents(), loadMyAgents()]);
loadAgentsForCurrentView();
showSuccess('Agent updated successfully');
} else {
const error = await response.json();
console.error('Update failed with response:', response.status, error);
showError(error.detail || `Failed to update agent (${response.status})`);
}
} catch (error) {
showError('Failed to update agent');
}
}
async function deleteAgent() {
try {
console.log('🗑️ Frontend: Starting deletion for agent:', currentAgentId);
const response = await fetch(`{{ base_path }}/api/agents/${currentAgentId}`, {
method: 'DELETE',
credentials: 'include'
});
console.log('🗑️ Frontend: Delete response status:', response.status, 'OK:', response.ok);
if (response.ok) {
console.log('🗑️ Frontend: Delete successful, closing modal and reloading');
// Close modal if it's open
const modal = bootstrap.Modal.getInstance(document.getElementById('agentModal'));
if (modal) modal.hide();
console.log('🗑️ Frontend: Reloading agents...');
await loadAgentsForCurrentView();
console.log('🗑️ Frontend: Showing success message');
showSuccess('Agent deleted successfully');
} else {
let errorMessage = 'Failed to delete agent';
try {
const error = await response.json();
errorMessage = error.detail || errorMessage;
} catch (e) {
// If response is not JSON, use default message
errorMessage = `Failed to delete agent (${response.status})`;
}
console.log('🗑️ Frontend: Delete failed:', errorMessage);
showError(errorMessage);
}
} catch (error) {
console.log('🗑️ Frontend: Exception during delete:', error);
showError('Failed to delete agent: ' + error.message);
}
}
function filterAgents() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const statusFilter = document.getElementById('statusFilter').value;
const auditFilter = document.getElementById('auditFilter').value;
const disciplineFilter = document.getElementById('disciplineFilter').value;
const ratingFilter = document.getElementById('ratingFilter').value;
let filtered = agents.filter(agent => {
const matchesSearch = agent.agent_name.toLowerCase().includes(searchTerm) ||
(agent.agent_description || '').toLowerCase().includes(searchTerm) ||
(agent.agent_tags || []).some(tag => tag.toLowerCase().includes(searchTerm));
const matchesStatus = !statusFilter || agent.agent_status === statusFilter;
const matchesAudit = !auditFilter ||
(auditFilter === 'audited' && agent.quality_audit_status) ||
(auditFilter === 'not_audited' && !agent.quality_audit_status);
const matchesDiscipline = !disciplineFilter || agent.discipline === disciplineFilter;
const matchesRating = !ratingFilter ||
(ratingFilter === 'unrated' && !agent.rating) ||
(ratingFilter !== 'unrated' && agent.rating && agent.rating >= parseFloat(ratingFilter));
return matchesSearch && matchesStatus && matchesAudit && matchesDiscipline && matchesRating;
});
displayAgents(filtered);
}
function sortAndDisplayAgents() {
const sortBy = document.getElementById('sortBy').value;
const sorted = [...agents].sort((a, b) => {
if (sortBy === 'name') {
return a.agent_name.localeCompare(b.agent_name);
} else if (sortBy === 'status') {
return (a.agent_status || 'Development').localeCompare(b.agent_status || 'Development');
} else if (sortBy === 'total_messages') {
// High to low (most used first), treat null/undefined as 0
const aVal = a.total_messages || 0;
const bVal = b.total_messages || 0;
return bVal - aVal; // Descending order
} else if (sortBy === 'total_tokens') {
const aVal = a.total_tokens || 0;
const bVal = b.total_tokens || 0;
return bVal - aVal; // Descending order
} else if (sortBy === 'unique_users') {
// High to low (most users first), treat null/undefined as 0
const aVal = a.unique_users || 0;
const bVal = b.unique_users || 0;
return bVal - aVal; // Descending order
} else if (sortBy === 'rating') {
return (b.rating || 0) - (a.rating || 0); // Highest rated first, unrated at bottom
} else {
// Default: created_at (newest first)
return new Date(b.agent_created_at) - new Date(a.agent_created_at);
}
});
displayAgents(sorted);
}
function updateAgentCounts() {
document.getElementById('agentCount').textContent = agents.length;
document.getElementById('allAgentsCount').textContent = allAgents.length;
document.getElementById('myAgentsCount').textContent = myAgents.length;
}
function formatDate(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
function getRiskFactorLabel(riskFactor) {
const labels = {
1: '1 - Very Low Risk',
2: '2 - Low Risk',
3: '3 - Medium Risk',
4: '4 - High Risk',
5: '5 - Very High Risk'
};
return labels[riskFactor] || 'Not Set';
}
function getRiskFactorBadge(riskFactor) {
if (!riskFactor) return '';
const colors = {
1: 'success', // Very Low Risk - Green
2: 'info', // Low Risk - Blue
3: 'warning', // Medium Risk - Yellow
4: 'orange', // High Risk - Orange (we'll style this)
5: 'danger' // Very High Risk - Red
};
return `<span class="badge bg-${colors[riskFactor] || 'secondary'} ms-1" title="${getRiskFactorLabel(riskFactor)}">Risk ${riskFactor}</span>`;
}
function showSuccess(message) {
// Simple alert - could be replaced with a toast notification
alert(message);
}
function showError(message) {
// Simple alert - could be replaced with a toast notification
alert('Error: ' + message);
}
function toggleEditRiskFactor() {
const qualityAuditStatus = document.getElementById('editQualityAuditStatus');
const riskFactorSection = document.getElementById('editRiskFactorSection');
const riskFactor = document.getElementById('editRiskFactor');
if (qualityAuditStatus.checked && currentUserIsAdmin) {
riskFactorSection.style.display = 'block';
riskFactor.required = true;
} else {
riskFactorSection.style.display = 'none';
riskFactor.required = false;
if (!qualityAuditStatus.checked) {
riskFactor.value = '';
}
}
}
function validateEditForm() {
const qualityAuditStatus = document.getElementById('editQualityAuditStatus');
const riskFactor = document.getElementById('editRiskFactor');
const discipline = document.getElementById('editAgentDiscipline');
// Validate Discipline is selected
if (!discipline.value) {
showError('Discipline is required!');
return false;
}
// Validate Risk Factor if Quality Audit is checked and user is admin
if (currentUserIsAdmin && qualityAuditStatus.checked && (!riskFactor.value || riskFactor.value === '')) {
showError('Risk Factor is required when Quality Audit is checked!');
return false;
}
return true;
}
function editAgent(agentId) {
console.log('Edit button clicked for agent:', agentId);
console.log('Current agents array:', agents);
currentAgentId = agentId;
showEditModal();
}
function confirmDelete(agentId) {
currentAgentId = agentId;
if (confirm('Are you sure you want to delete this agent? This action cannot be undone.')) {
deleteAgent();
}
}
function logout() {
window.location.href = '{{ base_path }}/logout';
}
let usageChart = null;
async function loadUsageChart(agentName, period = 'daily', startDate = null, endDate = null) {
try {
// Show loading state
document.getElementById('usageStatsContainer').style.display = 'block';
document.getElementById('usageChart').style.display = 'none';
// Build query parameters
const params = new URLSearchParams({ period });
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
// Fetch usage stats and chart data
const [statsResponse, chartResponse] = await Promise.all([
fetch(`{{ base_path }}/api/agents/${encodeURIComponent(agentName)}/usage?${params}`, {
credentials: 'include'
}),
fetch(`{{ base_path }}/api/agents/${encodeURIComponent(agentName)}/usage/chart?${params}`, {
credentials: 'include'
})
]);
if (!statsResponse.ok || !chartResponse.ok) {
throw new Error('Failed to load usage data');
}
const stats = await statsResponse.json();
const chartData = await chartResponse.json();
// Display statistics
const statsHtml = `
<div class="row text-center mb-3">
<div class="col">
<div class="border rounded p-2">
<h6 class="mb-1 text-primary">${stats.total_usage_count || 0}</h6>
<small class="text-muted">Total Messages</small>
</div>
</div>
<div class="col">
<div class="border rounded p-2">
<h6 class="mb-1 text-warning">${stats.total_tokens != null ? stats.total_tokens.toLocaleString() : 'N/A'}</h6>
<small class="text-muted">Total Tokens</small>
${stats.prompt_tokens != null || stats.completion_tokens != null ? `
<div class="mt-1" style="font-size: 0.7rem;">
<span class="text-muted">In: ${stats.prompt_tokens != null ? stats.prompt_tokens.toLocaleString() : '—'}</span> |
<span class="text-muted">Out: ${stats.completion_tokens != null ? stats.completion_tokens.toLocaleString() : '—'}</span>
</div>` : ''}
</div>
</div>
<div class="col">
<div class="border rounded p-2">
<h6 class="mb-1 text-success">${stats.conversation_count != null ? stats.conversation_count : 'N/A'}</h6>
<small class="text-muted">Conversations</small>
</div>
</div>
<div class="col">
<div class="border rounded p-2">
<h6 class="mb-1 text-info">${stats.unique_users != null ? stats.unique_users : 'N/A'}</h6>
<small class="text-muted">Unique Users</small>
</div>
</div>
</div>
<div class="row text-center mb-3">
<div class="col-6">
<div class="border rounded p-2">
<small class="text-muted">First Used: ${stats.first_usage ? formatDate(stats.first_usage) : 'Never'}</small>
</div>
</div>
<div class="col-6">
<div class="border rounded p-2">
<small class="text-muted">Last Used: ${stats.last_usage ? formatDate(stats.last_usage) : 'Never'}</small>
</div>
</div>
</div>
`;
document.getElementById('usageStatsContainer').innerHTML = statsHtml;
// Display chart if there's data
if (chartData.labels && chartData.labels.length > 0) {
document.getElementById('usageChart').style.display = 'block';
renderUsageChart(chartData);
} else {
document.getElementById('usageChart').style.display = 'none';
document.getElementById('usageStatsContainer').innerHTML +=
'<div class="text-center text-muted mt-3"><p>No usage data available for the selected period</p></div>';
}
// Setup event listeners for chart controls
setupChartEventListeners(agentName);
} catch (error) {
console.error('Error loading usage chart:', error);
document.getElementById('usageStatsContainer').innerHTML =
'<div class="text-center text-danger"><p>Failed to load usage data</p></div>';
}
}
function renderUsageChart(chartData) {
const ctx = document.getElementById('usageChart').getContext('2d');
// Destroy existing chart if it exists
if (usageChart) {
usageChart.destroy();
}
// Check if we have a second dataset (tokens) for dual-axis
const hasDualAxis = chartData.datasets.length > 1 && chartData.datasets.some(ds => ds.yAxisID === 'y1');
const scales = {
y: {
beginAtZero: true,
position: 'left',
title: {
display: hasDualAxis,
text: 'Messages'
},
ticks: {
stepSize: 1
}
}
};
if (hasDualAxis) {
scales.y1 = {
beginAtZero: true,
position: 'right',
title: {
display: true,
text: 'Tokens'
},
grid: {
drawOnChartArea: false
}
};
}
usageChart = new Chart(ctx, {
type: 'line',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: scales
}
});
}
// Star rating helper functions
function getCardStars(rating) {
let stars = '';
for (let i = 1; i <= 5; i++) {
if (i <= Math.floor(rating)) {
stars += '<span class="text-warning">&#9733;</span>';
} else if (i - 0.5 <= rating) {
stars += '<span class="text-warning">&#9733;</span>';
} else {
stars += '<span class="text-muted">&#9734;</span>';
}
}
return stars;
}
function getStarDisplay(rating, ratingCount) {
if (!rating) return '<span class="text-muted">Not yet rated</span>';
let stars = '';
for (let i = 1; i <= 5; i++) {
if (i <= Math.floor(rating)) {
stars += '<span class="text-warning">&#9733;</span>';
} else if (i - 0.5 <= rating) {
stars += '<span class="text-warning">&#9733;</span>';
} else {
stars += '<span class="text-muted">&#9734;</span>';
}
}
const countText = ratingCount ? ` (${ratingCount} rating${ratingCount !== 1 ? 's' : ''})` : '';
return stars + ` <small class="text-muted">${rating.toFixed(1)}${countText}</small>`;
}
function getClickableStars(currentRating, agentId) {
let html = '';
for (let i = 1; i <= 5; i++) {
const filled = i <= Math.round(currentRating);
html += `<span class="star-clickable ${filled ? 'text-warning' : 'text-muted'}" data-value="${i}" data-agent-id="${agentId}">&#9733;</span>`;
}
return html;
}
function setupDetailStarListeners(agentId, currentRating) {
const stars = document.querySelectorAll('#detailRatingDisplay .star-clickable');
stars.forEach(star => {
star.addEventListener('mouseenter', function() {
const val = parseInt(this.dataset.value);
stars.forEach(s => {
s.classList.toggle('text-warning', parseInt(s.dataset.value) <= val);
s.classList.toggle('text-muted', parseInt(s.dataset.value) > val);
});
});
star.addEventListener('mouseleave', function() {
const saved = currentRating;
stars.forEach(s => {
s.classList.toggle('text-warning', parseInt(s.dataset.value) <= Math.round(saved));
s.classList.toggle('text-muted', parseInt(s.dataset.value) > Math.round(saved));
});
});
star.addEventListener('click', async function() {
const val = parseInt(this.dataset.value);
try {
const response = await fetch(`{{ base_path }}/api/agents/${agentId}/rating`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ rating: val })
});
if (response.ok) {
const data = await response.json();
currentRating = val;
stars.forEach(s => {
s.classList.toggle('text-warning', parseInt(s.dataset.value) <= val);
s.classList.toggle('text-muted', parseInt(s.dataset.value) > val);
});
// Update average display next to stars
const container = document.getElementById('detailRatingDisplay');
const existingAvg = container.querySelector('.rating-avg-text');
if (existingAvg) existingAvg.remove();
if (data.rating) {
const avgSpan = document.createElement('small');
avgSpan.className = 'text-muted rating-avg-text';
avgSpan.textContent = ` Avg: ${data.rating.toFixed(1)} (${data.rating_count} rating${data.rating_count !== 1 ? 's' : ''})`;
container.appendChild(avgSpan);
}
// Update agents cache with new average
const idx = allAgents.findIndex(a => a.agent_id === agentId);
if (idx !== -1) { allAgents[idx].rating = data.rating; allAgents[idx].rating_count = data.rating_count; }
const myIdx = myAgents.findIndex(a => a.agent_id === agentId);
if (myIdx !== -1) { myAgents[myIdx].rating = data.rating; myAgents[myIdx].rating_count = data.rating_count; }
} else {
showError('Failed to save rating');
}
} catch (err) {
showError('Failed to save rating');
}
});
});
}
function setupChartEventListeners(agentName) {
// Period toggle listeners
document.querySelectorAll('input[name="period"]').forEach(radio => {
radio.addEventListener('change', function() {
if (this.checked) {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
loadUsageChart(agentName, this.value, startDate, endDate);
}
});
});
// Update chart button listener
document.getElementById('updateChart').addEventListener('click', function() {
const period = document.querySelector('input[name="period"]:checked').value;
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
loadUsageChart(agentName, period, startDate, endDate);
});
}
</script>
{% endblock %}