204 lines
6.1 KiB
HTML
204 lines
6.1 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<!-- Header -->
|
|
<section class="mb-2">
|
|
<div class="grid">
|
|
<div>
|
|
<h1>System Logs & Stats</h1>
|
|
<p class="text-muted">Monitor chat interactions and token usage.</p>
|
|
</div>
|
|
<div style="text-align: right;">
|
|
<a href="/admin" role="button" class="secondary outline">← Back to Dashboard</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Stats Grid -->
|
|
<section class="grid" id="stats-container">
|
|
<article class="card">
|
|
<header>Total Interactions</header>
|
|
<h2 id="stat-total-interactions">-</h2>
|
|
</article>
|
|
<article class="card">
|
|
<header>Vector Coverage</header>
|
|
<h2 id="stat-vector-coverage">-</h2>
|
|
<small class="text-muted" id="stat-vector-count">-</small>
|
|
</article>
|
|
<article class="card">
|
|
<header>Estimated Cost</header>
|
|
<h2 id="stat-cost">-</h2>
|
|
<small class="text-muted" id="stat-tokens">-</small>
|
|
</article>
|
|
<article class="card">
|
|
<header>Log Size</header>
|
|
<h2 id="stat-file-size">-</h2>
|
|
</article>
|
|
</section>
|
|
|
|
<!-- Role Stats -->
|
|
<section>
|
|
<h3>Role Usage</h3>
|
|
<div id="role-stats" class="grid-4">
|
|
<!-- Will be populated by JS -->
|
|
<article class="loading">Loading stats...</article>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Logs Table -->
|
|
<section style="margin-top: 2rem;">
|
|
<div class="grid">
|
|
<h3>Recent Interactions</h3>
|
|
<div style="text-align: right;">
|
|
<button onclick="loadData()" class="outline small">↻ Refresh</button>
|
|
</div>
|
|
</div>
|
|
|
|
<figure>
|
|
<table role="grid">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Time</th>
|
|
<th scope="col">Role</th>
|
|
<th scope="col">User</th>
|
|
<th scope="col">Interaction</th>
|
|
<th scope="col">Tokens</th>
|
|
<th scope="col">Model</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="logs-table-body">
|
|
<tr>
|
|
<td colspan="6" style="text-align: center;">Loading logs...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</figure>
|
|
</section>
|
|
|
|
<script>
|
|
async function loadData() {
|
|
try {
|
|
await Promise.all([loadStats(), loadLogs()]);
|
|
} catch (e) {
|
|
console.error("Error loading data:", e);
|
|
}
|
|
}
|
|
|
|
async function loadStats() {
|
|
const res = await fetch('/admin/logs/stats');
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
|
|
document.getElementById('stat-total-interactions').innerText = data.total_interactions;
|
|
|
|
// Vector Coverage
|
|
const rate = data.context_hit_rate_percent;
|
|
document.getElementById('stat-vector-coverage').innerText = rate + "%";
|
|
document.getElementById('stat-vector-count').innerText = `${data.context_found_count} hits`;
|
|
|
|
// Cost & Tokens
|
|
document.getElementById('stat-cost').innerText = "$" + data.estimated_cost_usd.toFixed(4);
|
|
document.getElementById('stat-tokens').innerText = `~${data.total_tokens_estimated} tokens`;
|
|
|
|
document.getElementById('stat-file-size').innerText = data.file_size_mb + " MB";
|
|
|
|
// Render Role Stats
|
|
const roleContainer = document.getElementById('role-stats');
|
|
roleContainer.innerHTML = '';
|
|
|
|
// Sort roles by usage (desc)
|
|
const sortedRoles = Object.entries(data.characters).sort((a, b) => b[1] - a[1]);
|
|
|
|
sortedRoles.forEach(([roleId, count]) => {
|
|
const tokens = data.tokens_by_role[roleId] || 0;
|
|
const article = document.createElement('article');
|
|
article.className = 'card small-card';
|
|
article.innerHTML = `
|
|
<header class="capitalize"><strong>${roleId}</strong></header>
|
|
<div class="stat-value">${count} <small>chats</small></div>
|
|
<div class="stat-sub">${tokens} <small>tokens</small></div>
|
|
`;
|
|
roleContainer.appendChild(article);
|
|
});
|
|
}
|
|
|
|
async function loadLogs() {
|
|
const res = await fetch('/admin/logs/data?limit=50');
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
|
|
const tbody = document.getElementById('logs-table-body');
|
|
tbody.innerHTML = '';
|
|
|
|
data.logs.forEach(log => {
|
|
const row = document.createElement('tr');
|
|
|
|
// Format time
|
|
const date = new Date(log.timestamp);
|
|
const timeStr = date.toLocaleString();
|
|
|
|
// Truncate text
|
|
const q = log.interaction.question;
|
|
const qShort = q.length > 50 ? q.substring(0, 50) + '...' : q;
|
|
|
|
row.innerHTML = `
|
|
<td><small>${timeStr}</small></td>
|
|
<td class="capitalize">${log.character.name}</td>
|
|
<td>${log.user.email || log.user.id || 'Anonymous'}</td>
|
|
<td>
|
|
<strong>Q:</strong> ${escapeHtml(qShort)}<br>
|
|
<small class="text-muted">Sources: ${log.rag.sources_count}</small>
|
|
</td>
|
|
<td>${log.tokens_estimated}</td>
|
|
<td><small>${log.ai.model}</small></td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
// Initial load
|
|
document.addEventListener('DOMContentLoaded', loadData);
|
|
|
|
// Auto-refresh every 30s
|
|
setInterval(loadData, 30000);
|
|
</script>
|
|
|
|
<style>
|
|
.capitalize {
|
|
text-transform: capitalize;
|
|
}
|
|
|
|
.small-card {
|
|
padding: 1rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.stat-sub {
|
|
font-size: 0.9rem;
|
|
color: var(--muted-color);
|
|
}
|
|
|
|
/* Grid 4 for role stats */
|
|
.grid-4 {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
</style>
|
|
|
|
{% endblock %} |