Add discipline field, star rating system, and dashboard filtering
- Add discipline (business category) and rating (1-5 stars) fields to agent models
- Discipline dropdown on registration form (required) and edit modals (both user and admin)
- Interactive star rating widget with immediate save via PUT /api/agents/{id}/rating
- Discipline filter and rating sort on agent management dashboard
- Purple discipline badge and gold star badge on agent cards
- CSV export/import support for discipline, rating, and total_tokens
- Initialize total_tokens on manually-created agents for consistent CSV exports
- Search agents by discipline field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0e7940801b
commit
a50ef3ec64
8 changed files with 400 additions and 23 deletions
19
CLAUDE.md
19
CLAUDE.md
|
|
@ -57,11 +57,11 @@ Create `.env` file with:
|
|||
### Data Layer
|
||||
|
||||
**models.py**: Pydantic models for:
|
||||
- `AiAgent`: Core agent model with comprehensive fields
|
||||
- `AiAgent`: Core agent model with comprehensive fields (includes `discipline`, `rating`)
|
||||
- `UsageTimelineEntry`: Daily usage data including `message_count` and `token_count`
|
||||
- `UserCreate/UserResponse`: User management models
|
||||
- `AiAgentCreate/AiAgentResponse`: API request/response models (includes `total_tokens`)
|
||||
- `AgentCollectorCreate`: Collector API input model (includes `total_tokens`)
|
||||
- `AiAgentCreate/AiAgentResponse`: API request/response models (includes `total_tokens`, `discipline`, `rating`)
|
||||
- `AgentCollectorCreate`: Collector API input model (includes `total_tokens`, `discipline`)
|
||||
- `AgentUsageStatsResponse`: Usage statistics response (includes `total_tokens`)
|
||||
|
||||
**crud.py**: Database operations using Motor (async MongoDB driver):
|
||||
|
|
@ -118,9 +118,20 @@ Located in `templates/` directory:
|
|||
- Status tracking (Active, Inactive, Development, Deprecated)
|
||||
- Rich metadata: tags, userbase, department, contact person
|
||||
- Search functionality across multiple fields
|
||||
- Filtering by status and audit status (Audited / Not Audited)
|
||||
- Filtering by status, audit status (Audited / Not Audited), and discipline
|
||||
- Admin can view/manage all agents
|
||||
|
||||
### Discipline & Star Rating
|
||||
- `discipline` field classifies agents into business categories: Strategy, Creative, Oversight including delivery, Optimization, Back Office including operations
|
||||
- Required on registration form, optional on edit (to support legacy agents)
|
||||
- `rating` field is a 1-5 star rating for quality assessment by discipline leads
|
||||
- Interactive star rating widget in detail modal saves immediately via `PUT /api/agents/{id}/rating`
|
||||
- Star rating also editable in the edit modal (both agent management page and admin dashboard)
|
||||
- Dashboard supports filtering by discipline and sorting by rating
|
||||
- Discipline badge (purple) and star rating badge displayed on agent cards
|
||||
- Both fields included in CSV export/import
|
||||
- Discipline passed through collector API; rating is human-only (not in collector)
|
||||
|
||||
### Token Usage Tracking
|
||||
- `total_tokens` field on agents tracks cumulative LLM token consumption
|
||||
- `token_count` per day in usage timeline entries alongside `message_count`
|
||||
|
|
|
|||
11
crud.py
11
crud.py
|
|
@ -151,6 +151,12 @@ async def create_agent(agent_data: dict, user_id: str, skip_time_check: bool = F
|
|||
agent_doc["quality_audit_updated_by_name"] = None
|
||||
if "risk_factor" not in agent_doc:
|
||||
agent_doc["risk_factor"] = None
|
||||
if "discipline" not in agent_doc:
|
||||
agent_doc["discipline"] = None
|
||||
if "rating" not in agent_doc:
|
||||
agent_doc["rating"] = None
|
||||
if "total_tokens" not in agent_doc:
|
||||
agent_doc["total_tokens"] = None
|
||||
result = await agents_collection.insert_one(agent_doc)
|
||||
agent_doc["_id"] = result.inserted_id
|
||||
return agent_doc
|
||||
|
|
@ -198,7 +204,8 @@ async def search_agents(search_term: str, user_id: str = None):
|
|||
{"agent_name": {"$regex": search_term, "$options": "i"}},
|
||||
{"agent_description": {"$regex": search_term, "$options": "i"}},
|
||||
{"agent_tags": {"$regex": search_term, "$options": "i"}},
|
||||
{"agent_department": {"$regex": search_term, "$options": "i"}}
|
||||
{"agent_department": {"$regex": search_term, "$options": "i"}},
|
||||
{"discipline": {"$regex": search_term, "$options": "i"}}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -403,7 +410,7 @@ def _agent_data_differs(existing_agent: dict, new_agent_data: dict) -> bool:
|
|||
"agent_description", "agent_purpose", "agent_version", "agent_status",
|
||||
"agent_location", "agent_department", "agent_contact_person",
|
||||
"agent_tags", "agent_userbase", "agent_capabilities", "agent_metadata",
|
||||
"url"
|
||||
"url", "discipline"
|
||||
]
|
||||
|
||||
for field in comparable_fields:
|
||||
|
|
|
|||
41
main.py
41
main.py
|
|
@ -225,6 +225,8 @@ def create_agent_response(agent: dict) -> models.AiAgentResponse:
|
|||
quality_audit_updated_by_name=agent.get("quality_audit_updated_by_name"),
|
||||
risk_factor=agent.get("risk_factor"),
|
||||
last_edited_by=agent.get("last_edited_by"),
|
||||
discipline=agent.get("discipline"),
|
||||
rating=agent.get("rating"),
|
||||
created_by=agent["created_by"],
|
||||
# Usage tracking fields
|
||||
usage_timeline=agent.get("usage_timeline"),
|
||||
|
|
@ -279,6 +281,7 @@ def map_agent_collector_to_internal(collector_data: models.AgentCollectorCreate)
|
|||
"agent_tags": collector_data.tags,
|
||||
"agent_metadata": collector_data.metadata,
|
||||
"url": collector_data.url,
|
||||
"discipline": collector_data.discipline,
|
||||
# Usage tracking fields
|
||||
"usage_timeline": usage_timeline,
|
||||
"conversation_count": collector_data.conversation_count,
|
||||
|
|
@ -570,6 +573,7 @@ async def agent_register_form(
|
|||
agent_location: str = Form(None),
|
||||
agent_department: str = Form(None),
|
||||
agent_contact_person: str = Form(None),
|
||||
discipline: str = Form(...),
|
||||
agent_tags: str = Form(None),
|
||||
agent_userbase: str = Form(None),
|
||||
agent_capabilities: str = Form(None),
|
||||
|
|
@ -595,6 +599,7 @@ async def agent_register_form(
|
|||
"agent_location": agent_location,
|
||||
"agent_department": agent_department,
|
||||
"agent_contact_person": agent_contact_person,
|
||||
"discipline": discipline,
|
||||
}
|
||||
|
||||
# Process tags, userbase, and capabilities (convert comma-separated to lists)
|
||||
|
|
@ -886,6 +891,28 @@ async def update_agent(agent_id: str, agent: models.AiAgentCreate, current_user:
|
|||
|
||||
return create_agent_response(updated_agent)
|
||||
|
||||
@app.put("/api/agents/{agent_id}/rating")
|
||||
async def update_agent_rating(agent_id: str, request: Request, current_user: dict = Depends(get_current_user_from_cookie)):
|
||||
"""Update only the star rating of an agent (lightweight endpoint for immediate saves)"""
|
||||
existing_agent = await crud.get_agent_by_id(agent_id)
|
||||
if not existing_agent:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
|
||||
if not can_user_edit_agent(existing_agent, current_user):
|
||||
raise HTTPException(status_code=403, detail="Not authorized to rate this agent")
|
||||
|
||||
body = await request.json()
|
||||
rating_value = body.get("rating")
|
||||
|
||||
if rating_value is None or not (1 <= float(rating_value) <= 5):
|
||||
raise HTTPException(status_code=422, detail="Rating must be between 1 and 5")
|
||||
|
||||
updated = await crud.update_agent(agent_id, {"rating": float(rating_value)}, last_edited_by=current_user.get("email"))
|
||||
if not updated:
|
||||
raise HTTPException(status_code=500, detail="Failed to update rating")
|
||||
|
||||
return {"message": "Rating updated", "rating": float(rating_value)}
|
||||
|
||||
@app.delete("/api/agents/{agent_id}")
|
||||
async def delete_agent(agent_id: str, current_user: dict = Depends(get_current_user_from_cookie)):
|
||||
print(f"🗑️ DELETE attempt - Agent ID: {agent_id}, User ID: {current_user['_id']}")
|
||||
|
|
@ -1086,7 +1113,9 @@ async def export_agents_csv(current_user: dict = Depends(require_admin)):
|
|||
"risk_factor",
|
||||
"last_edited_by",
|
||||
"created_by",
|
||||
"total_tokens"
|
||||
"total_tokens",
|
||||
"discipline",
|
||||
"rating"
|
||||
]
|
||||
|
||||
writer = csv.DictWriter(output, fieldnames=fieldnames)
|
||||
|
|
@ -1121,7 +1150,9 @@ async def export_agents_csv(current_user: dict = Depends(require_admin)):
|
|||
"risk_factor": str(agent.get("risk_factor", "")) if agent.get("risk_factor") is not None else "",
|
||||
"last_edited_by": agent.get("last_edited_by", ""),
|
||||
"created_by": agent.get("created_by", ""),
|
||||
"total_tokens": str(agent.get("total_tokens", "")) if agent.get("total_tokens") is not None else ""
|
||||
"total_tokens": str(agent.get("total_tokens", "")) if agent.get("total_tokens") is not None else "",
|
||||
"discipline": agent.get("discipline", ""),
|
||||
"rating": str(agent.get("rating", "")) if agent.get("rating") is not None else ""
|
||||
}
|
||||
writer.writerow(row)
|
||||
|
||||
|
|
@ -1180,7 +1211,10 @@ async def import_agents_csv(
|
|||
"agent_contact_person": row.get("agent_contact_person"),
|
||||
"url": row.get("url"),
|
||||
"quality_audit_status": row.get("quality_audit_status", "False").lower() == "true",
|
||||
"risk_factor": int(row.get("risk_factor")) if row.get("risk_factor") else None
|
||||
"risk_factor": int(row.get("risk_factor")) if row.get("risk_factor") else None,
|
||||
"discipline": row.get("discipline") or None,
|
||||
"rating": float(row.get("rating")) if row.get("rating") else None,
|
||||
"total_tokens": int(row.get("total_tokens")) if row.get("total_tokens") else None
|
||||
}
|
||||
|
||||
# Handle lists (pipe separated)
|
||||
|
|
@ -1323,6 +1357,7 @@ async def create_agent_collector(
|
|||
# Update agent document with new usage data (replace strategy)
|
||||
update_fields = {
|
||||
"url": internal_data.get("url"),
|
||||
"discipline": internal_data.get("discipline"),
|
||||
"usage_timeline": internal_data.get("usage_timeline"),
|
||||
"conversation_count": internal_data.get("conversation_count"),
|
||||
"unique_users": internal_data.get("unique_users"),
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ class AiAgent(BaseModel):
|
|||
quality_audit_updated_by_name: str | None = Field(default=None, title="Admin user name who updated quality audit")
|
||||
risk_factor: int | None = Field(default=None, title="Risk factor rating (1-5)", ge=1, le=5)
|
||||
last_edited_by: str | None = Field(default=None, title="Email of user who last edited this agent")
|
||||
discipline: str | None = Field(default=None, title="Business discipline/category", max_length=100)
|
||||
rating: float | None = Field(default=None, title="Star rating (1-5)", ge=1, le=5)
|
||||
|
||||
|
||||
|
||||
|
|
@ -98,6 +100,8 @@ class AiAgentCreate(BaseModel):
|
|||
quality_audit_updated_by_name: Optional[str] = None
|
||||
risk_factor: Optional[int] = Field(default=None, ge=1, le=5)
|
||||
last_edited_by: Optional[str] = None
|
||||
discipline: Optional[str] = None
|
||||
rating: Optional[float] = Field(default=None, ge=1, le=5)
|
||||
|
||||
class AiAgentResponse(BaseModel):
|
||||
agent_id: str
|
||||
|
|
@ -123,6 +127,8 @@ class AiAgentResponse(BaseModel):
|
|||
quality_audit_updated_by_name: Optional[str] = None
|
||||
risk_factor: Optional[int] = None
|
||||
last_edited_by: Optional[str] = None
|
||||
discipline: Optional[str] = None
|
||||
rating: Optional[float] = None
|
||||
created_by: str
|
||||
|
||||
# Usage tracking fields (new)
|
||||
|
|
@ -152,6 +158,7 @@ class AgentCollectorCreate(BaseModel):
|
|||
tags: Optional[list[str]] = None
|
||||
metadata: Optional[dict] = None
|
||||
url: Optional[str] = None
|
||||
discipline: Optional[str] = None
|
||||
|
||||
# Usage tracking fields (new)
|
||||
usage_timeline: Optional[List[UsageTimelineEntry]] = None
|
||||
|
|
|
|||
|
|
@ -402,4 +402,25 @@ h2 {
|
|||
.container h2 {
|
||||
-webkit-text-fill-color: var(--text-dark);
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
/* Star rating styles */
|
||||
.star-interactive, .star-clickable {
|
||||
cursor: pointer;
|
||||
font-size: 1.3rem;
|
||||
transition: color 0.2s, transform 0.2s;
|
||||
color: #dee2e6;
|
||||
}
|
||||
|
||||
.star-interactive:hover, .star-clickable:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.star-rating-edit {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Discipline badge */
|
||||
.bg-purple {
|
||||
background-color: #6f42c1 !important;
|
||||
}
|
||||
|
|
@ -416,7 +416,36 @@
|
|||
<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>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">
|
||||
<i class="fas fa-star me-2"></i>Rating
|
||||
</label>
|
||||
<div class="star-rating-edit" id="editStarRating">
|
||||
<span class="star-interactive" data-value="1">★</span>
|
||||
<span class="star-interactive" data-value="2">★</span>
|
||||
<span class="star-interactive" data-value="3">★</span>
|
||||
<span class="star-interactive" data-value="4">★</span>
|
||||
<span class="star-interactive" data-value="5">★</span>
|
||||
</div>
|
||||
<input type="hidden" id="editAgentRating" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="editQualityAuditSection">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="editQualityAuditStatus" onchange="toggleAdminRiskFactor()">
|
||||
|
|
@ -637,6 +666,21 @@
|
|||
color: white !important;
|
||||
}
|
||||
|
||||
.star-interactive {
|
||||
cursor: pointer;
|
||||
font-size: 1.3rem;
|
||||
transition: color 0.2s, transform 0.2s;
|
||||
color: #dee2e6;
|
||||
}
|
||||
|
||||
.star-interactive:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.star-rating-edit {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
|
|
@ -661,6 +705,7 @@ let allAgents = [];
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadAdminData();
|
||||
setupEventListeners();
|
||||
setupEditStarRating();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
|
|
@ -1118,7 +1163,10 @@ async function editAgentAdmin(agentId) {
|
|||
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 || '';
|
||||
document.getElementById('editAgentRating').value = agent.rating || '';
|
||||
updateEditStarDisplay(agent.rating || 0);
|
||||
|
||||
// Handle Quality Audit field (admin always has access)
|
||||
document.getElementById('editQualityAuditStatus').checked = agent.quality_audit_status || false;
|
||||
|
||||
|
|
@ -1173,9 +1221,11 @@ async function handleEditAgentSubmit(e) {
|
|||
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,
|
||||
rating: document.getElementById('editAgentRating').value ? parseFloat(document.getElementById('editAgentRating').value) : null,
|
||||
quality_audit_status: document.getElementById('editQualityAuditStatus').checked
|
||||
};
|
||||
|
||||
|
||||
// Add Risk Factor
|
||||
const riskFactorValue = document.getElementById('editRiskFactor').value;
|
||||
agentData.risk_factor = riskFactorValue ? parseInt(riskFactorValue) : null;
|
||||
|
|
@ -1346,5 +1396,45 @@ async function handleResetPasswordSubmit(e) {
|
|||
showError('Failed to reset password');
|
||||
}
|
||||
}
|
||||
|
||||
// Star rating functions for edit modal
|
||||
function setupEditStarRating() {
|
||||
const container = document.getElementById('editStarRating');
|
||||
if (!container) return;
|
||||
const stars = container.querySelectorAll('.star-interactive');
|
||||
const hiddenInput = document.getElementById('editAgentRating');
|
||||
|
||||
stars.forEach(star => {
|
||||
star.addEventListener('mouseenter', function() {
|
||||
const val = parseInt(this.dataset.value);
|
||||
stars.forEach(s => {
|
||||
s.style.color = parseInt(s.dataset.value) <= val ? '#ffc107' : '#dee2e6';
|
||||
});
|
||||
});
|
||||
star.addEventListener('mouseleave', function() {
|
||||
const current = parseInt(hiddenInput.value) || 0;
|
||||
stars.forEach(s => {
|
||||
s.style.color = parseInt(s.dataset.value) <= current ? '#ffc107' : '#dee2e6';
|
||||
});
|
||||
});
|
||||
star.addEventListener('click', function() {
|
||||
const val = parseInt(this.dataset.value);
|
||||
hiddenInput.value = val;
|
||||
stars.forEach(s => {
|
||||
s.style.color = parseInt(s.dataset.value) <= val ? '#ffc107' : '#dee2e6';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateEditStarDisplay(rating) {
|
||||
const container = document.getElementById('editStarRating');
|
||||
if (!container) return;
|
||||
const stars = container.querySelectorAll('.star-interactive');
|
||||
const val = Math.round(rating) || 0;
|
||||
stars.forEach(s => {
|
||||
s.style.color = parseInt(s.dataset.value) <= val ? '#ffc107' : '#dee2e6';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-3 mb-3 mb-md-0">
|
||||
<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...">
|
||||
|
|
@ -99,11 +99,22 @@
|
|||
<option value="not_audited">Not Audited</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3 mb-md-0">
|
||||
<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>
|
||||
</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>
|
||||
|
|
@ -316,7 +327,36 @@
|
|||
<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>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">
|
||||
<i class="fas fa-star me-2"></i>Rating
|
||||
</label>
|
||||
<div class="star-rating-edit" id="editStarRating">
|
||||
<span class="star-interactive" data-value="1">★</span>
|
||||
<span class="star-interactive" data-value="2">★</span>
|
||||
<span class="star-interactive" data-value="3">★</span>
|
||||
<span class="star-interactive" data-value="4">★</span>
|
||||
<span class="star-interactive" data-value="5">★</span>
|
||||
</div>
|
||||
<input type="hidden" id="editAgentRating" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="editQualityAuditSection">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="editQualityAuditStatus" onchange="toggleEditRiskFactor()">
|
||||
|
|
@ -462,12 +502,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
loadAgentsForCurrentView();
|
||||
});
|
||||
setupEventListeners();
|
||||
setupEditStarRating();
|
||||
});
|
||||
|
||||
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('sortBy').addEventListener('change', sortAndDisplayAgents);
|
||||
document.getElementById('refreshBtn').addEventListener('click', function() {
|
||||
// Reload both datasets to ensure counts are accurate
|
||||
|
|
@ -622,6 +664,8 @@ function displayAgents(agentsToShow) {
|
|||
${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="badge bg-warning text-dark" title="Rating: ${agent.rating}/5"><i class="fas fa-star me-1"></i>${agent.rating.toFixed(1)}</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>` : ''}
|
||||
|
|
@ -710,6 +754,10 @@ async function showAgentDetails(agentId) {
|
|||
<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></td><td>
|
||||
<span id="detailRatingDisplay" data-agent-id="${agent.agent_id}">${agent.rating ? getStarDisplay(agent.rating) : '<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>' :
|
||||
|
|
@ -852,7 +900,17 @@ async function showAgentDetails(agentId) {
|
|||
|
||||
const modal = new bootstrap.Modal(document.getElementById('agentModal'));
|
||||
modal.show();
|
||||
|
||||
|
||||
// Add clickable star rating if user can edit
|
||||
if (canEdit) {
|
||||
const ratingContainer = document.getElementById('detailRatingDisplay');
|
||||
if (ratingContainer) {
|
||||
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);
|
||||
|
||||
|
|
@ -905,7 +963,10 @@ async function showEditModal() {
|
|||
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 || '';
|
||||
document.getElementById('editAgentRating').value = agent.rating || '';
|
||||
updateEditStarDisplay(agent.rating || 0);
|
||||
|
||||
// Handle Quality Audit field
|
||||
const qualityAuditCheckbox = document.getElementById('editQualityAuditStatus');
|
||||
const qualityAuditSection = document.getElementById('editQualityAuditSection');
|
||||
|
|
@ -975,9 +1036,11 @@ async function updateAgent(e) {
|
|||
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)
|
||||
agent_capabilities: document.getElementById('editAgentCapabilities').value.split(',').map(s => s.trim()).filter(s => s),
|
||||
discipline: document.getElementById('editAgentDiscipline').value || null,
|
||||
rating: document.getElementById('editAgentRating').value ? parseFloat(document.getElementById('editAgentRating').value) : null
|
||||
};
|
||||
|
||||
|
||||
// Add Quality Audit status and Risk Factor if user is admin
|
||||
if (currentUserIsAdmin) {
|
||||
agentData.quality_audit_status = document.getElementById('editQualityAuditStatus').checked;
|
||||
|
|
@ -1064,6 +1127,7 @@ 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;
|
||||
|
||||
let filtered = agents.filter(agent => {
|
||||
const matchesSearch = agent.agent_name.toLowerCase().includes(searchTerm) ||
|
||||
|
|
@ -1073,7 +1137,8 @@ function filterAgents() {
|
|||
const matchesAudit = !auditFilter ||
|
||||
(auditFilter === 'audited' && agent.quality_audit_status) ||
|
||||
(auditFilter === 'not_audited' && !agent.quality_audit_status);
|
||||
return matchesSearch && matchesStatus && matchesAudit;
|
||||
const matchesDiscipline = !disciplineFilter || agent.discipline === disciplineFilter;
|
||||
return matchesSearch && matchesStatus && matchesAudit && matchesDiscipline;
|
||||
});
|
||||
|
||||
displayAgents(filtered);
|
||||
|
|
@ -1100,6 +1165,8 @@ function sortAndDisplayAgents() {
|
|||
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);
|
||||
|
|
@ -1176,13 +1243,20 @@ function toggleEditRiskFactor() {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1354,6 +1428,118 @@ function renderUsageChart(chartData) {
|
|||
});
|
||||
}
|
||||
|
||||
// Star rating helper functions
|
||||
function getStarDisplay(rating) {
|
||||
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>';
|
||||
}
|
||||
}
|
||||
return stars + ` <small class="text-muted">(${rating.toFixed(1)})</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) {
|
||||
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 agents cache
|
||||
const idx = allAgents.findIndex(a => a.agent_id === agentId);
|
||||
if (idx !== -1) allAgents[idx].rating = val;
|
||||
const myIdx = myAgents.findIndex(a => a.agent_id === agentId);
|
||||
if (myIdx !== -1) myAgents[myIdx].rating = val;
|
||||
} else {
|
||||
showError('Failed to save rating');
|
||||
}
|
||||
} catch (err) {
|
||||
showError('Failed to save rating');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Edit modal star rating interaction
|
||||
function setupEditStarRating() {
|
||||
const container = document.getElementById('editStarRating');
|
||||
if (!container) return;
|
||||
const stars = container.querySelectorAll('.star-interactive');
|
||||
const hiddenInput = document.getElementById('editAgentRating');
|
||||
|
||||
stars.forEach(star => {
|
||||
star.addEventListener('mouseenter', function() {
|
||||
const val = parseInt(this.dataset.value);
|
||||
stars.forEach(s => {
|
||||
s.style.color = parseInt(s.dataset.value) <= val ? '#ffc107' : '#dee2e6';
|
||||
});
|
||||
});
|
||||
star.addEventListener('mouseleave', function() {
|
||||
const current = parseInt(hiddenInput.value) || 0;
|
||||
stars.forEach(s => {
|
||||
s.style.color = parseInt(s.dataset.value) <= current ? '#ffc107' : '#dee2e6';
|
||||
});
|
||||
});
|
||||
star.addEventListener('click', function() {
|
||||
const val = parseInt(this.dataset.value);
|
||||
hiddenInput.value = val;
|
||||
stars.forEach(s => {
|
||||
s.style.color = parseInt(s.dataset.value) <= val ? '#ffc107' : '#dee2e6';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateEditStarDisplay(rating) {
|
||||
const container = document.getElementById('editStarRating');
|
||||
if (!container) return;
|
||||
const stars = container.querySelectorAll('.star-interactive');
|
||||
const val = Math.round(rating) || 0;
|
||||
stars.forEach(s => {
|
||||
s.style.color = parseInt(s.dataset.value) <= val ? '#ffc107' : '#dee2e6';
|
||||
});
|
||||
}
|
||||
|
||||
function setupChartEventListeners(agentName) {
|
||||
// Period toggle listeners
|
||||
document.querySelectorAll('input[name="period"]').forEach(radio => {
|
||||
|
|
|
|||
|
|
@ -99,6 +99,19 @@
|
|||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<label for="agentDiscipline" class="form-label">
|
||||
<i class="fas fa-layer-group me-2"></i>Discipline *
|
||||
</label>
|
||||
<select name="discipline" class="form-select" id="agentDiscipline" required>
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-4">
|
||||
<label for="agentLocation" class="form-label">
|
||||
<i class="fas fa-map-marker-alt me-2"></i>Location
|
||||
|
|
@ -319,7 +332,14 @@ document.getElementById('agentForm').addEventListener('submit', function(e) {
|
|||
alert('Agent tool is required!');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const discipline = document.getElementById('agentDiscipline').value;
|
||||
if (!discipline) {
|
||||
e.preventDefault();
|
||||
alert('Discipline is required!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate Risk Factor if Quality Audit is checked
|
||||
if (qualityAuditStatus.checked && (!riskFactor.value || riskFactor.value === '')) {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue