596 lines
28 KiB
HTML
596 lines
28 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Usage Analytics — Nexus One AI</title>
|
||
<link rel="stylesheet" href="style.css?v=4">
|
||
<style>
|
||
/* ── Toolbar ── */
|
||
.an-toolbar { display:flex; align-items:center; gap:10px; margin-bottom:24px; flex-wrap:wrap; }
|
||
.an-period-btn {
|
||
padding:7px 14px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer;
|
||
border:1.5px solid var(--bdr); background:var(--navy2); color:var(--med); font-family:inherit; transition:.15s;
|
||
}
|
||
.an-period-btn.active { border-color:var(--purple); color:var(--purple); background:rgba(124,58,237,.1); }
|
||
.an-scope-badge { margin-left:auto; font-size:12px; padding:4px 12px; border-radius:20px; font-weight:600; }
|
||
.an-scope-badge.admin { background:rgba(34,197,94,.12); color:#15803D; }
|
||
.an-scope-badge.personal { background:rgba(124,58,237,.1); color:var(--purple); }
|
||
.an-export-btn {
|
||
padding:7px 14px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer;
|
||
border:1.5px solid var(--bdr); background:var(--navy2); color:var(--med); font-family:inherit; transition:.15s;
|
||
display:flex; align-items:center; gap:6px;
|
||
}
|
||
.an-export-btn:hover { border-color:var(--purple); color:var(--purple); }
|
||
|
||
/* ── Stat tiles ── */
|
||
.an-tiles { display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin-bottom:24px; }
|
||
@media(max-width:900px){ .an-tiles { grid-template-columns:repeat(2,1fr); } }
|
||
@media(max-width:540px){ .an-tiles { grid-template-columns:1fr; } }
|
||
.an-tile {
|
||
background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:20px 22px;
|
||
display:flex; flex-direction:column; gap:4px;
|
||
}
|
||
.an-tile-icon { font-size:20px; margin-bottom:4px; }
|
||
.an-tile-val { font-size:32px; font-weight:800; color:var(--ink); line-height:1; }
|
||
.an-tile-lbl { font-size:12px; color:var(--lt); }
|
||
.an-tile-delta { font-size:11px; font-weight:600; margin-top:2px; }
|
||
.an-tile-delta.up { color:#15803D; }
|
||
.an-tile-delta.down { color:#DC2626; }
|
||
.an-tile-delta.flat { color:var(--lt); }
|
||
|
||
/* ── Cards ── */
|
||
.an-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:22px 24px; margin-bottom:20px; }
|
||
.an-card-head { display:flex; align-items:center; gap:10px; margin-bottom:16px; }
|
||
.an-card-title { font-size:15px; font-weight:700; color:var(--ink); flex:1; }
|
||
.an-card-sub { font-size:12px; color:var(--lt); }
|
||
|
||
/* ── Charts ── */
|
||
.an-chart-wrap canvas { max-width:100%; display:block; }
|
||
|
||
/* ── Tables ── */
|
||
.an-table-wrap { border:1px solid var(--bdr); border-radius:10px; overflow:hidden; }
|
||
table.an-table { width:100%; border-collapse:collapse; }
|
||
.an-table th { background:var(--bg); padding:10px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.5px; color:var(--lt); border-bottom:1px solid var(--bdr); }
|
||
.an-table td { padding:10px 14px; border-bottom:1px solid var(--bdr); font-size:13px; color:var(--ink); }
|
||
.an-table tr:last-child td { border-bottom:none; }
|
||
.an-table tr:hover td { background:var(--bg); }
|
||
.an-bar-bg { background:var(--bg); border-radius:4px; height:5px; margin-top:5px; }
|
||
.an-bar-fill { background:var(--purple); height:5px; border-radius:4px; transition:width .4s; }
|
||
.an-empty { text-align:center; color:var(--lt); padding:32px 0; font-size:13px; }
|
||
|
||
/* ── Grid layouts ── */
|
||
.an-grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
|
||
.an-grid-3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:20px; }
|
||
@media(max-width:900px){ .an-grid-2,.an-grid-3 { grid-template-columns:1fr; } }
|
||
|
||
/* ── Feature usage pills ── */
|
||
.an-feat-row { display:flex; align-items:center; gap:10px; margin-bottom:12px; }
|
||
.an-feat-row:last-child { margin-bottom:0; }
|
||
.an-feat-icon { font-size:16px; width:28px; flex-shrink:0; }
|
||
.an-feat-name { font-size:13px; font-weight:600; color:var(--ink); width:130px; flex-shrink:0; }
|
||
.an-feat-bar-wrap { flex:1; background:var(--bg); border-radius:4px; height:8px; }
|
||
.an-feat-bar { height:8px; border-radius:4px; transition:width .4s; }
|
||
.an-feat-count { font-size:12px; color:var(--lt); width:50px; text-align:right; flex-shrink:0; }
|
||
|
||
/* ── Peak hours ── */
|
||
.an-heatmap { display:grid; grid-template-columns:repeat(24,1fr); gap:3px; margin-top:8px; }
|
||
.an-heat-cell {
|
||
aspect-ratio:1; border-radius:3px; background:var(--bg);
|
||
cursor:default; position:relative;
|
||
}
|
||
.an-heat-cell:hover::after {
|
||
content:attr(data-tip); position:absolute; bottom:calc(100% + 4px); left:50%; transform:translateX(-50%);
|
||
background:var(--ink); color:white; font-size:10px; padding:3px 7px; border-radius:5px; white-space:nowrap; z-index:10;
|
||
}
|
||
.an-hour-labels { display:grid; grid-template-columns:repeat(24,1fr); gap:3px; margin-top:4px; }
|
||
.an-hour-label { font-size:9px; color:var(--lt); text-align:center; }
|
||
|
||
/* ── Token budget ── */
|
||
.an-budget-wrap { display:flex; align-items:center; gap:16px; }
|
||
.an-budget-bar-outer { flex:1; background:var(--bg); border-radius:8px; height:12px; }
|
||
.an-budget-bar-inner { height:12px; border-radius:8px; transition:width .5s; background:var(--purple); }
|
||
.an-budget-bar-inner.warn { background:#D97706; }
|
||
.an-budget-bar-inner.danger { background:#DC2626; }
|
||
.an-budget-pct { font-size:14px; font-weight:700; color:var(--ink); width:44px; text-align:right; flex-shrink:0; }
|
||
.an-budget-lbl { font-size:12px; color:var(--lt); margin-top:6px; }
|
||
|
||
/* ── Notice ── */
|
||
.an-notice { background:rgba(124,58,237,.08); border:1px solid rgba(124,58,237,.25); border-radius:10px; padding:12px 16px; font-size:13px; color:var(--purple); margin-bottom:20px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="topnav">
|
||
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
|
||
<nav>
|
||
<a href="index.html">Home</a>
|
||
<a href="quickstart.html">Quick Start</a>
|
||
<a href="prompts.html">Prompt Library</a>
|
||
<a href="usecases.html">Use Cases</a>
|
||
<span class="nav-sep"></span>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn">Help ▾</button>
|
||
<div class="nav-drop-menu">
|
||
<span class="nav-drop-cat">LEARN /</span>
|
||
<a href="quickstart.html">Quick Start</a>
|
||
<a href="models.html">Models</a>
|
||
<span class="nav-drop-cat">SUPPORT /</span>
|
||
<a href="troubleshooting.html">Troubleshoot</a>
|
||
<a href="faq.html">FAQ</a>
|
||
<span class="nav-drop-cat">MORE /</span>
|
||
<a href="glossary.html">Glossary</a>
|
||
<a href="whats-new.html">What's New</a>
|
||
</div>
|
||
</div>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn active">Admin ▾</button>
|
||
<div class="nav-drop-menu nav-drop-menu-wide">
|
||
<span class="nav-drop-cat">DOCS /</span>
|
||
<a href="security.html">Security & Privacy</a>
|
||
<a href="admin.html">Admin Guide</a>
|
||
<span class="nav-drop-cat">MONITOR /</span>
|
||
<a href="dashboard.html">Dashboard</a>
|
||
<a href="analytics.html" class="active">Usage Analytics</a>
|
||
<a href="audit.html">Audit Log</a>
|
||
<a href="feedback.html">Feedback & Ratings</a>
|
||
<span class="nav-drop-cat">MANAGE /</span>
|
||
<a href="users.html">Users</a>
|
||
<a href="teams.html">Teams</a>
|
||
<a href="models-admin.html">Model Manager</a>
|
||
<a href="training.html">Training</a>
|
||
<a href="knowledge.html">Knowledge Base</a>
|
||
<span class="nav-drop-cat">TOOLS /</span>
|
||
<a href="apikeys.html">API Keys</a>
|
||
<a href="benchmark.html">Benchmarking</a>
|
||
<a href="model-compare.html">Model Compare</a>
|
||
<a href="api-playground.html">API Playground</a>
|
||
<a href="guardrails.html">Guardrails</a>
|
||
<a href="rag-quality.html">RAG Quality</a>
|
||
<a href="router.html">Model Router</a>
|
||
<a href="connectors.html">Connectors</a>
|
||
<span class="nav-drop-cat">SYSTEM /</span>
|
||
<a href="console.html">Console</a>
|
||
<a href="settings.html">Settings</a>
|
||
</div>
|
||
</div>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn">AI Tools ▾</button>
|
||
<div class="nav-drop-menu">
|
||
<span class="nav-drop-cat">INTELLIGENCE /</span>
|
||
<a href="documents.html">Document Intelligence</a>
|
||
<a href="chat-multi.html">Multimodal Chat</a>
|
||
<a href="prompt-studio.html">Prompt Studio</a>
|
||
<a href="meeting.html">Meeting Assistant</a>
|
||
<span class="nav-drop-cat">AUTOMATION /</span>
|
||
<a href="agents.html">Agent Builder</a>
|
||
<a href="schedules.html">Scheduled Jobs</a>
|
||
<a href="workflows.html">Workflow Automation</a>
|
||
<span class="nav-drop-cat">QUALITY /</span>
|
||
<a href="evals.html">AI Eval Suite</a>
|
||
<a href="chatrooms.html">Chat Rooms</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
<a href="notifications.html" style="position:relative">🔔</a>
|
||
<span class="badge" data-brand="tier">Basic Tier</span>
|
||
<div id="nav-org-logo" class="nav-org-logo"></div>
|
||
</header>
|
||
|
||
<div class="page-hero">
|
||
<div class="label">Admin · Analytics</div>
|
||
<h1>Usage Analytics</h1>
|
||
<p>Track AI usage across users, models, features, and time — all on-premises, all private.</p>
|
||
</div>
|
||
|
||
<div class="content">
|
||
|
||
<!-- Toolbar -->
|
||
<div class="an-toolbar">
|
||
<button class="an-period-btn active" onclick="setPeriod(7,this)">Last 7 days</button>
|
||
<button class="an-period-btn" onclick="setPeriod(30,this)">Last 30 days</button>
|
||
<button class="an-period-btn" onclick="setPeriod(90,this)">Last 90 days</button>
|
||
<button class="an-export-btn" onclick="exportCsv()">⬇️ Export CSV</button>
|
||
<span class="an-scope-badge admin" id="scope-badge">🏢 Org-wide view</span>
|
||
</div>
|
||
|
||
<div class="an-notice" id="an-notice" style="display:none"></div>
|
||
|
||
<!-- 6 Stat tiles -->
|
||
<div class="an-tiles">
|
||
<div class="an-tile">
|
||
<div class="an-tile-icon">🔢</div>
|
||
<div class="an-tile-val" id="stat-queries">—</div>
|
||
<div class="an-tile-lbl">Total Queries</div>
|
||
<div class="an-tile-delta up" id="delta-queries"></div>
|
||
</div>
|
||
<div class="an-tile">
|
||
<div class="an-tile-icon">🪙</div>
|
||
<div class="an-tile-val" id="stat-tokens">—</div>
|
||
<div class="an-tile-lbl">Tokens Processed</div>
|
||
<div class="an-tile-delta up" id="delta-tokens"></div>
|
||
</div>
|
||
<div class="an-tile">
|
||
<div class="an-tile-icon">⚡</div>
|
||
<div class="an-tile-val" id="stat-avg">—</div>
|
||
<div class="an-tile-lbl">Avg Response Time</div>
|
||
<div class="an-tile-delta flat" id="delta-avg"></div>
|
||
</div>
|
||
<div class="an-tile">
|
||
<div class="an-tile-icon">👥</div>
|
||
<div class="an-tile-val" id="stat-users">—</div>
|
||
<div class="an-tile-lbl">Active Users</div>
|
||
<div class="an-tile-delta up" id="delta-users"></div>
|
||
</div>
|
||
<div class="an-tile">
|
||
<div class="an-tile-icon">📄</div>
|
||
<div class="an-tile-val" id="stat-docs">—</div>
|
||
<div class="an-tile-lbl">Documents Processed</div>
|
||
<div class="an-tile-delta up" id="delta-docs"></div>
|
||
</div>
|
||
<div class="an-tile">
|
||
<div class="an-tile-icon">🤖</div>
|
||
<div class="an-tile-val" id="stat-agents">—</div>
|
||
<div class="an-tile-lbl">Agent Runs</div>
|
||
<div class="an-tile-delta up" id="delta-agents"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Daily queries chart -->
|
||
<div class="an-card">
|
||
<div class="an-card-head">
|
||
<div class="an-card-title">Queries per Day</div>
|
||
<div class="an-card-sub" id="chart-subtitle"></div>
|
||
</div>
|
||
<div class="an-chart-wrap">
|
||
<canvas id="chart-daily" height="100"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Feature usage + Token budget -->
|
||
<div class="an-grid-2" style="margin-bottom:20px">
|
||
|
||
<div class="an-card" style="margin-bottom:0">
|
||
<div class="an-card-head">
|
||
<div class="an-card-title">Usage by Feature</div>
|
||
<div class="an-card-sub">queries routed per tool</div>
|
||
</div>
|
||
<div id="feature-usage"></div>
|
||
</div>
|
||
|
||
<div class="an-card" style="margin-bottom:0">
|
||
<div class="an-card-head">
|
||
<div class="an-card-title">Token Budget</div>
|
||
<div class="an-card-sub" id="budget-period-lbl">this period</div>
|
||
</div>
|
||
<div id="token-budget"></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Peak hours -->
|
||
<div class="an-card">
|
||
<div class="an-card-head">
|
||
<div class="an-card-title">Peak Activity Hours</div>
|
||
<div class="an-card-sub">average queries per hour of day (local time)</div>
|
||
</div>
|
||
<div class="an-heatmap" id="heatmap"></div>
|
||
<div class="an-hour-labels" id="hour-labels"></div>
|
||
</div>
|
||
|
||
<!-- Models + Top users -->
|
||
<div class="an-grid-2">
|
||
<div class="an-card" style="margin-bottom:0">
|
||
<div class="an-card-head">
|
||
<div class="an-card-title">Model Usage</div>
|
||
</div>
|
||
<div id="models-table"></div>
|
||
</div>
|
||
<div class="an-card" id="users-card" style="margin-bottom:0">
|
||
<div class="an-card-head">
|
||
<div class="an-card-title">Top Users</div>
|
||
<div class="an-card-sub">admin view</div>
|
||
</div>
|
||
<div id="users-table"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<footer>
|
||
<p>Nexus One AI · Powered by Cezen · <span data-brand="tier">Basic Tier</span></p>
|
||
<p>Questions? <a href="mailto:support@cezentech.com">support@cezentech.com</a> · <a href="https://cezentech.com" target="_blank">cezentech.com</a></p>
|
||
</footer>
|
||
|
||
<script>
|
||
const _API = '/api';
|
||
let currentDays = 7;
|
||
let currentData = null;
|
||
|
||
/* ── Mock data generator ─────────────────────────────────────────────────── */
|
||
function mockData(days) {
|
||
const seed = days * 137;
|
||
const rng = (min, max, s=1) => min + Math.abs(Math.sin(seed * s) * (max - min)) | 0;
|
||
|
||
const queries = rng(420, 980, 1) * (days / 7);
|
||
const tokens = queries * rng(280, 640, 2);
|
||
const daily = [];
|
||
const today = new Date();
|
||
for (let i = days - 1; i >= 0; i--) {
|
||
const d = new Date(today); d.setDate(d.getDate() - i);
|
||
const key = d.toISOString().slice(0,10);
|
||
const weekday = d.getDay();
|
||
const base = weekday === 0 || weekday === 6 ? rng(8,30,i+3) : rng(40,120,i+3);
|
||
daily.push({ day: key, queries: base });
|
||
}
|
||
|
||
return {
|
||
is_admin: true,
|
||
totals: {
|
||
total_queries: Math.round(queries),
|
||
total_tokens: Math.round(tokens),
|
||
avg_duration_ms: rng(820, 2400, 5),
|
||
active_users: rng(8, 24, 6),
|
||
docs_processed: rng(60, 340, 7),
|
||
agent_runs: rng(15, 80, 8),
|
||
},
|
||
deltas: {
|
||
queries: '+12%', tokens: '+18%', avg: '−4%', users: '+3', docs: '+28%', agents: '+9%'
|
||
},
|
||
daily,
|
||
models: [
|
||
{ model: 'llama3:70b', queries: rng(180, 420, 10) },
|
||
{ model: 'mistral:7b', queries: rng(90, 200, 11) },
|
||
{ model: 'llava:13b', queries: rng(40, 120, 12) },
|
||
{ model: 'codellama:34b', queries: rng(20, 80, 13) },
|
||
{ model: 'mixtral:8x7b', queries: rng(10, 50, 14) },
|
||
],
|
||
top_users: [
|
||
{ username: 'rajesh.k', queries: rng(80, 180, 20), dept: 'Finance' },
|
||
{ username: 'priya.m', queries: rng(60, 130, 21), dept: 'Legal' },
|
||
{ username: 'arun.s', queries: rng(40, 100, 22), dept: 'HR' },
|
||
{ username: 'deepa.v', queries: rng(30, 80, 23), dept: 'IT' },
|
||
{ username: 'kiran.p', queries: rng(20, 60, 24), dept: 'Operations' },
|
||
],
|
||
features: [
|
||
{ name: 'Multimodal Chat', icon: '💬', queries: rng(150, 400, 30), color: '#7C3AED' },
|
||
{ name: 'Document Intelligence', icon: '📄', queries: rng(80, 200, 31), color: '#EC4899' },
|
||
{ name: 'Agent Builder', icon: '🤖', queries: rng(40, 100, 32), color: '#0EA5E9' },
|
||
{ name: 'Workflows', icon: '⚙️', queries: rng(20, 80, 33), color: '#10B981' },
|
||
{ name: 'Meeting Assistant', icon: '🎙️', queries: rng(15, 60, 34), color: '#F59E0B' },
|
||
{ name: 'Knowledge Base', icon: '🧠', queries: rng(10, 40, 35), color: '#6366F1' },
|
||
],
|
||
peak_hours: Array.from({length:24}, (_,h) => {
|
||
const isWork = h >= 9 && h <= 18;
|
||
const isLunch = h === 13;
|
||
return isLunch ? rng(5,15,h+40) : isWork ? rng(20,80,h+40) : rng(0,8,h+40);
|
||
}),
|
||
token_budget: {
|
||
used: Math.round(tokens),
|
||
total: 50_000_000,
|
||
label: `${(tokens/1_000_000).toFixed(1)}M / 50M tokens`
|
||
}
|
||
};
|
||
}
|
||
|
||
/* ── Period ──────────────────────────────────────────────────────────────── */
|
||
function setPeriod(days, btn) {
|
||
currentDays = days;
|
||
document.querySelectorAll('.an-period-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
loadAnalytics();
|
||
}
|
||
|
||
/* ── Load ────────────────────────────────────────────────────────────────── */
|
||
async function loadAnalytics() {
|
||
try {
|
||
const res = await fetch(`${_API}/analytics/summary?days=${currentDays}`, {credentials:'include'});
|
||
if (!res.ok) throw new Error();
|
||
currentData = await res.json();
|
||
} catch(e) {
|
||
currentData = mockData(currentDays);
|
||
}
|
||
render(currentData);
|
||
}
|
||
|
||
/* ── Render ──────────────────────────────────────────────────────────────── */
|
||
function fmt(n) {
|
||
if (n >= 1_000_000) return (n/1_000_000).toFixed(1) + 'M';
|
||
if (n >= 1_000) return (n/1_000).toFixed(1) + 'K';
|
||
return Math.round(n).toLocaleString();
|
||
}
|
||
|
||
function render(d) {
|
||
// Scope badge
|
||
const badge = document.getElementById('scope-badge');
|
||
const notice = document.getElementById('an-notice');
|
||
if (d.is_admin) {
|
||
badge.textContent = '🏢 Org-wide view'; badge.className = 'an-scope-badge admin';
|
||
notice.style.display = 'none';
|
||
} else {
|
||
badge.textContent = '👤 My usage'; badge.className = 'an-scope-badge personal';
|
||
notice.textContent = 'Showing your personal usage only. Admins see organisation-wide data.';
|
||
notice.style.display = '';
|
||
}
|
||
|
||
// Tiles
|
||
const t = d.totals;
|
||
const deltas = d.deltas || {};
|
||
setTile('queries', fmt(t.total_queries), deltas.queries, 'up');
|
||
setTile('tokens', fmt(t.total_tokens), deltas.tokens, 'up');
|
||
const avgMs = t.avg_duration_ms;
|
||
setTile('avg', avgMs >= 1000 ? (avgMs/1000).toFixed(1)+'s' : Math.round(avgMs)+'ms', deltas.avg, 'flat');
|
||
setTile('users', String(t.active_users || '—'), deltas.users, 'up');
|
||
setTile('docs', fmt(t.docs_processed || 0), deltas.docs, 'up');
|
||
setTile('agents', fmt(t.agent_runs || 0), deltas.agents, 'up');
|
||
|
||
// Charts
|
||
document.getElementById('chart-subtitle').textContent = `${currentDays}-day total: ${fmt(d.totals.total_queries)} queries`;
|
||
renderDailyChart(d.daily);
|
||
renderFeatureUsage(d.features || []);
|
||
renderTokenBudget(d.token_budget || {used: t.total_tokens, total: 50_000_000});
|
||
renderHeatmap(d.peak_hours || []);
|
||
renderBarTable('models-table', d.models, 'model', 'queries', 'Model', 'Queries');
|
||
if (d.is_admin && d.top_users) {
|
||
document.getElementById('users-card').style.display = '';
|
||
renderBarTable('users-table', d.top_users, 'username', 'queries', 'User', 'Queries', r => r.dept ? `<span style="font-size:11px;color:var(--lt)">${escHtml(r.dept)}</span>` : '');
|
||
} else {
|
||
document.getElementById('users-card').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function setTile(key, val, delta, dir) {
|
||
document.getElementById('stat-'+key).textContent = val;
|
||
const el = document.getElementById('delta-'+key);
|
||
if (el && delta) {
|
||
el.textContent = delta + ' vs prev period';
|
||
el.className = 'an-tile-delta ' + (delta.startsWith('+') ? 'up' : delta.startsWith('−') || delta.startsWith('-') ? 'down' : 'flat');
|
||
}
|
||
}
|
||
|
||
/* ── Daily chart ─────────────────────────────────────────────────────────── */
|
||
function renderDailyChart(daily) {
|
||
const canvas = document.getElementById('chart-daily');
|
||
const ctx = canvas.getContext('2d');
|
||
const labels = [], data = [];
|
||
const byDay = {};
|
||
(daily || []).forEach(r => { byDay[r.day] = r.queries; });
|
||
const today = new Date();
|
||
for (let i = currentDays - 1; i >= 0; i--) {
|
||
const d = new Date(today); d.setDate(d.getDate() - i);
|
||
const key = d.toISOString().slice(0,10);
|
||
labels.push(key.slice(5));
|
||
data.push(byDay[key] || 0);
|
||
}
|
||
const W = canvas.parentElement.clientWidth;
|
||
canvas.width = W; canvas.height = 100;
|
||
const H = canvas.height;
|
||
const padL=36, padR=12, padT=12, padB=28;
|
||
const cW=W-padL-padR, cH=H-padT-padB;
|
||
const maxVal = Math.max(...data, 1);
|
||
ctx.clearRect(0,0,W,H);
|
||
// grid
|
||
ctx.strokeStyle='rgba(0,0,0,.06)'; ctx.lineWidth=1;
|
||
for (let i=0;i<=4;i++) {
|
||
const y=padT+cH-(i/4)*cH;
|
||
ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke();
|
||
ctx.fillStyle='#9CA3AF'; ctx.font='10px system-ui'; ctx.textAlign='right';
|
||
ctx.fillText(fmt(Math.round((maxVal*i)/4)), padL-4, y+3);
|
||
}
|
||
if (!data.length) return;
|
||
const step = cW/(data.length-1||1);
|
||
// gradient fill
|
||
const grad=ctx.createLinearGradient(0,padT,0,padT+cH);
|
||
grad.addColorStop(0,'rgba(124,58,237,.22)'); grad.addColorStop(1,'rgba(124,58,237,.01)');
|
||
ctx.beginPath();
|
||
data.forEach((v,i)=>{ const x=padL+i*step, y=padT+cH-(v/maxVal)*cH; i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); });
|
||
ctx.lineTo(padL+(data.length-1)*step,padT+cH); ctx.lineTo(padL,padT+cH); ctx.closePath();
|
||
ctx.fillStyle=grad; ctx.fill();
|
||
// line
|
||
ctx.beginPath(); ctx.strokeStyle='#7C3AED'; ctx.lineWidth=2; ctx.lineJoin='round';
|
||
data.forEach((v,i)=>{ const x=padL+i*step, y=padT+cH-(v/maxVal)*cH; i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); });
|
||
ctx.stroke();
|
||
// dots
|
||
data.forEach((v,i)=>{ const x=padL+i*step, y=padT+cH-(v/maxVal)*cH; ctx.beginPath(); ctx.arc(x,y,3,0,Math.PI*2); ctx.fillStyle='#7C3AED'; ctx.fill(); });
|
||
// x-labels
|
||
const every=Math.ceil(data.length/8);
|
||
ctx.fillStyle='#9CA3AF'; ctx.font='10px system-ui'; ctx.textAlign='center';
|
||
labels.forEach((lbl,i)=>{ if(i%every===0||i===labels.length-1) ctx.fillText(lbl,padL+i*step,H-6); });
|
||
}
|
||
|
||
/* ── Feature usage ───────────────────────────────────────────────────────── */
|
||
function renderFeatureUsage(features) {
|
||
const el = document.getElementById('feature-usage');
|
||
if (!features.length) { el.innerHTML = '<div class="an-empty">No data</div>'; return; }
|
||
const max = Math.max(...features.map(f=>f.queries), 1);
|
||
el.innerHTML = features.map(f => {
|
||
const pct = Math.round((f.queries/max)*100);
|
||
return `<div class="an-feat-row">
|
||
<span class="an-feat-icon">${f.icon||'🔹'}</span>
|
||
<span class="an-feat-name">${escHtml(f.name)}</span>
|
||
<div class="an-feat-bar-wrap"><div class="an-feat-bar" style="width:${pct}%;background:${f.color||'var(--purple)'}"></div></div>
|
||
<span class="an-feat-count">${fmt(f.queries)}</span>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
/* ── Token budget ────────────────────────────────────────────────────────── */
|
||
function renderTokenBudget(budget) {
|
||
const el = document.getElementById('token-budget');
|
||
document.getElementById('budget-period-lbl').textContent = `last ${currentDays} days`;
|
||
const pct = Math.min(100, Math.round((budget.used/budget.total)*100));
|
||
const cls = pct >= 90 ? 'danger' : pct >= 70 ? 'warn' : '';
|
||
el.innerHTML = `
|
||
<div class="an-budget-wrap">
|
||
<div class="an-budget-bar-outer"><div class="an-budget-bar-inner ${cls}" style="width:${pct}%"></div></div>
|
||
<div class="an-budget-pct">${pct}%</div>
|
||
</div>
|
||
<div class="an-budget-lbl">${fmt(budget.used)} of ${fmt(budget.total)} tokens used</div>
|
||
<div style="margin-top:16px;display:flex;flex-direction:column;gap:6px">
|
||
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--lt)">
|
||
<span>Avg tokens per query</span><span style="color:var(--ink);font-weight:600">${currentData?.totals?.total_queries ? fmt(Math.round(budget.used/currentData.totals.total_queries)) : '—'}</span>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--lt)">
|
||
<span>Estimated monthly</span><span style="color:var(--ink);font-weight:600">${fmt(Math.round(budget.used*(30/currentDays)))}</span>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--lt)">
|
||
<span>Budget remaining</span><span style="color:${cls==='danger'?'#DC2626':cls==='warn'?'#D97706':'#15803D'};font-weight:600">${fmt(budget.total-budget.used)}</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Peak hours heatmap ──────────────────────────────────────────────────── */
|
||
function renderHeatmap(hourData) {
|
||
const hm = document.getElementById('heatmap');
|
||
const hl = document.getElementById('hour-labels');
|
||
const max = Math.max(...hourData, 1);
|
||
hm.innerHTML = hourData.map((v,h) => {
|
||
const pct = v/max;
|
||
const alpha = (0.08 + pct*0.85).toFixed(2);
|
||
const tip = `${h}:00 — ${v} avg queries`;
|
||
return `<div class="an-heat-cell" style="background:rgba(124,58,237,${alpha})" data-tip="${tip}"></div>`;
|
||
}).join('');
|
||
hl.innerHTML = hourData.map((_,h) => `<div class="an-hour-label">${h%3===0?h+'h':''}</div>`).join('');
|
||
}
|
||
|
||
/* ── Bar table ───────────────────────────────────────────────────────────── */
|
||
function renderBarTable(containerId, rows, nameKey, valKey, nameHeader, valHeader, extraFn) {
|
||
const el = document.getElementById(containerId);
|
||
if (!rows || !rows.length) { el.innerHTML = '<div class="an-empty">No data yet</div>'; return; }
|
||
const max = Math.max(...rows.map(r=>r[valKey]), 1);
|
||
let html = `<div class="an-table-wrap"><table class="an-table">
|
||
<tr><th>${nameHeader}</th><th style="text-align:right">${valHeader}</th></tr>`;
|
||
rows.forEach(r => {
|
||
const pct = Math.round((r[valKey]/max)*100);
|
||
const extra = extraFn ? extraFn(r) : '';
|
||
html += `<tr><td>
|
||
<div style="display:flex;align-items:center;gap:6px"><span style="font-weight:600">${escHtml(String(r[nameKey]||'—'))}</span>${extra}</div>
|
||
<div class="an-bar-bg"><div class="an-bar-fill" style="width:${pct}%"></div></div>
|
||
</td><td style="text-align:right;color:var(--lt);font-variant-numeric:tabular-nums">${fmt(r[valKey])}</td></tr>`;
|
||
});
|
||
el.innerHTML = html + '</table></div>';
|
||
}
|
||
|
||
/* ── Export CSV ──────────────────────────────────────────────────────────── */
|
||
function exportCsv() {
|
||
if (!currentData) return;
|
||
const rows = [['Date','Queries']];
|
||
(currentData.daily||[]).forEach(r => rows.push([r.day, r.queries]));
|
||
const csv = rows.map(r=>r.join(',')).join('\n');
|
||
const a = document.createElement('a');
|
||
a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv);
|
||
a.download = `cezen-analytics-${currentDays}d.csv`;
|
||
a.click();
|
||
}
|
||
|
||
function escHtml(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
loadAnalytics();
|
||
window.addEventListener('resize', () => { if(currentData) renderDailyChart(currentData.daily); });
|
||
</script>
|
||
|
||
<script src="auth.js"></script>
|
||
<script src="branding.js"></script>
|
||
</body>
|
||
</html>
|