Swap the muddy #f3ae3e palette for the real OLIVER brand pulled from the master
PPT template: yellow #FFCB05 + near-black #1A1A1A + off-white #F6F7F7, Montserrat
font. White-first page with a brand-yellow highlight rectangle behind page titles,
stat tiles with yellow left-strip, and a short yellow accent line under each
card section title — picks up the template's "01" chapter-marker rhythm.
Fixes two production bugs along the way:
- Nav stays pinned at top while page scrolls. The conflicting
`.navbar { position: relative !important }` rule was removed from nav.html
so the `position: fixed` from style.css can take effect.
- Clicking admin tabs no longer scrolls the page. Converted
`<a href="#users">` to `<button data-bs-target="#users">` (Bootstrap 5's
recommended pattern), so the anchor jump can't happen.
Other refinements: table padding loosened, `transform: scale` row hover
removed (jittery on dense rows), modal headers switched to near-black,
Chart.js palette aligned with brand tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1763 lines
No EOL
73 KiB
HTML
1763 lines
No EOL
73 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 class="page-title">{{ 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 %}
|
|
|
|
<!-- Incomplete-registration banner (populated by JS) -->
|
|
<div class="row mb-3" id="incompleteBannerRow" style="display:none;">
|
|
<div class="col-12">
|
|
<div class="alert alert-warning d-flex align-items-center justify-content-between" role="alert" style="border-radius:10px;">
|
|
<div>
|
|
<i class="fas fa-exclamation-circle me-2"></i>
|
|
<strong id="incompleteBannerCount"></strong>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-warning" id="filterIncompleteBtn">
|
|
<i class="fas fa-filter me-1"></i>Show only incomplete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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="Optimisation">Optimisation</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>
|
|
<!-- Second filter row: governance dimensions + compliance quick filter -->
|
|
<div class="row align-items-center mt-3">
|
|
<div class="col-md-3 mb-3 mb-md-0">
|
|
<select class="form-select" id="businessEntityFilter">
|
|
<option value="">All Business Entities</option>
|
|
<option value="OLIVER">OLIVER</option>
|
|
<option value="DARE">DARE</option>
|
|
<option value="Brandtech Group">Brandtech Group</option>
|
|
<option value="Pencil">Pencil</option>
|
|
<option value="Jellyfish">Jellyfish</option>
|
|
<option value="Adjust">Adjust</option>
|
|
<option value="Other">Other</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3 mb-3 mb-md-0">
|
|
<select class="form-select" id="agentTypeFilter">
|
|
<option value="">All Agent Types</option>
|
|
<option value="Utility">Utility</option>
|
|
<option value="Functional">Functional</option>
|
|
<option value="Supervisory">Supervisory</option>
|
|
<option value="Guardian">Guardian</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3 mb-3 mb-md-0">
|
|
<select class="form-select" id="autonomyFilter">
|
|
<option value="">All Autonomy Levels</option>
|
|
<option value="Human-Led">Human-Led</option>
|
|
<option value="Hybrid">Hybrid</option>
|
|
<option value="Autopilot">Autopilot</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3 mb-3 mb-md-0">
|
|
<button type="button" class="btn btn-outline-danger w-100" id="complianceRiskBtn"
|
|
title="Show only agents with PII=Yes, IP=Shared/TBD, or Autopilot autonomy">
|
|
<i class="fas fa-shield-alt me-1"></i>Compliance risks
|
|
</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_purpose[:80] if agent.agent_purpose else 'No purpose listed' }}{% if agent.agent_purpose and agent.agent_purpose|length > 80 %}...{% 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="Optimisation">Optimisation</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">★</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">★★</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">★★★</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">★★★★</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">★★★★★</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: 6px;
|
|
background: var(--brand-yellow);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--brand-dark);
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.agent-avatar {
|
|
width: 35px;
|
|
height: 35px;
|
|
border-radius: 4px;
|
|
background: var(--brand-yellow);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--brand-dark);
|
|
font-family: 'Montserrat', sans-serif;
|
|
font-weight: 700;
|
|
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('businessEntityFilter').addEventListener('change', filterAgents);
|
|
document.getElementById('agentTypeFilter').addEventListener('change', filterAgents);
|
|
document.getElementById('autonomyFilter').addEventListener('change', filterAgents);
|
|
document.getElementById('complianceRiskBtn').addEventListener('click', function() {
|
|
complianceRiskOnly = !complianceRiskOnly;
|
|
this.classList.toggle('btn-outline-danger', !complianceRiskOnly);
|
|
this.classList.toggle('btn-danger', complianceRiskOnly);
|
|
this.innerHTML = complianceRiskOnly
|
|
? '<i class="fas fa-times me-1"></i>Showing risks — clear'
|
|
: '<i class="fas fa-shield-alt me-1"></i>Compliance risks';
|
|
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');
|
|
});
|
|
|
|
// Incomplete-registration banner: load count and wire the filter button
|
|
loadIncompleteCount();
|
|
document.getElementById('filterIncompleteBtn').addEventListener('click', function() {
|
|
incompleteOnly = !incompleteOnly;
|
|
this.classList.toggle('btn-warning', !incompleteOnly);
|
|
this.classList.toggle('btn-secondary', incompleteOnly);
|
|
this.innerHTML = incompleteOnly
|
|
? '<i class="fas fa-times me-1"></i>Showing incomplete only — clear'
|
|
: '<i class="fas fa-filter me-1"></i>Show only incomplete';
|
|
filterAgents();
|
|
});
|
|
}
|
|
|
|
let incompleteOnly = false;
|
|
let complianceRiskOnly = false;
|
|
async function loadIncompleteCount() {
|
|
try {
|
|
const r = await fetch('{{ base_path }}/api/agents/incomplete', { credentials: 'include' });
|
|
if (!r.ok) return;
|
|
const list = await r.json();
|
|
if (list.length > 0) {
|
|
document.getElementById('incompleteBannerRow').style.display = '';
|
|
document.getElementById('incompleteBannerCount').textContent =
|
|
list.length + (list.length === 1 ? ' agent needs' : ' agents need') + ' registration completed';
|
|
}
|
|
} catch (e) { /* non-blocking */ }
|
|
}
|
|
|
|
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_purpose || 'No purpose listed').length > 100 ? (agent.agent_purpose || '').substring(0, 100) + '...' : (agent.agent_purpose || 'No purpose listed')}</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.registration_complete === false ? `<span class="badge bg-warning text-dark" title="This agent's registration form has not been completed"><i class="fas fa-exclamation-circle me-1"></i>Incomplete</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">
|
|
${(agent.registration_complete === false && canUserEditAgent(agent)) ? `
|
|
<a class="btn btn-warning btn-sm me-1" href="{{ base_path }}/agent-complete/${agent.agent_id}" onclick="event.stopPropagation();" title="Complete this agent's registration">
|
|
<i class="fas fa-check-circle me-1"></i>Complete
|
|
</a>
|
|
` : ''}
|
|
${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));
|
|
const matchesIncomplete = !incompleteOnly || agent.registration_complete === false;
|
|
|
|
const businessEntityFilter = document.getElementById('businessEntityFilter').value;
|
|
const agentTypeFilter = document.getElementById('agentTypeFilter').value;
|
|
const autonomyFilter = document.getElementById('autonomyFilter').value;
|
|
const matchesEntity = !businessEntityFilter || agent.business_entity === businessEntityFilter;
|
|
const matchesType = !agentTypeFilter || agent.agent_classification === agentTypeFilter;
|
|
const matchesAutonomy = !autonomyFilter || agent.autonomy_level === autonomyFilter;
|
|
|
|
// Compliance risks: any of PII=Yes, IP=Shared/TBD, Autopilot autonomy
|
|
const matchesCompliance = !complianceRiskOnly || (
|
|
((agent.pii && agent.pii.handles_pii === true)) ||
|
|
(agent.ip_ownership === 'Shared/TBD') ||
|
|
(agent.autonomy_level === 'Autopilot')
|
|
);
|
|
|
|
return matchesSearch && matchesStatus && matchesAudit && matchesDiscipline && matchesRating
|
|
&& matchesIncomplete && matchesEntity && matchesType && matchesAutonomy && matchesCompliance;
|
|
});
|
|
|
|
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">★</span>';
|
|
} else if (i - 0.5 <= rating) {
|
|
stars += '<span class="text-warning">★</span>';
|
|
} else {
|
|
stars += '<span class="text-muted">☆</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">★</span>';
|
|
} else if (i - 0.5 <= rating) {
|
|
stars += '<span class="text-warning">★</span>';
|
|
} else {
|
|
stars += '<span class="text-muted">☆</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}">★</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 %} |