feat(billing): CG4b — Chart.js donut + modul-tabell per token-forbruk
This commit is contained in:
parent
2404ed47cb
commit
87345b151d
|
|
@ -253,6 +253,7 @@
|
|||
.md-viewer hr{border:none;border-top:1px solid var(--border);margin:20px 0}
|
||||
.md-viewer-loading{display:flex;align-items:center;justify-content:center;height:200px;color:var(--text-faint);font-family:var(--mono);font-size:13px;gap:10px}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
|
|
@ -593,16 +594,62 @@ async function loadBilling(){
|
|||
h+='</div>'
|
||||
}else{ h+='<div class="sec-title" style="color:var(--green)">✅ Ingen anomalier</div>' }
|
||||
const mods = mod?.modules||mod?.data||[]
|
||||
const cols = ['#7c5cff','#00d4a8','#ffb547','#ff5c7c','#3ecf8e','#5c8fff','#ff9f7c','#c084fc']
|
||||
if(mods.length){
|
||||
const cols=['#7c5cff','#00d4a8','#ffb547','#ff5c7c','#3ecf8e']
|
||||
h+='<div class="sec-title" style="margin-top:24px">Token-forbruk per modul</div><table class="mod-table"><thead><tr><th>Modul</th><th>Tokens</th><th>Input</th><th>Output</th></tr></thead><tbody>'
|
||||
h+='<div class="sec-title" style="margin-top:24px">Token-forbruk per modul</div>'
|
||||
h+='<table class="mod-table"><thead><tr><th>Modul</th><th>Tokens</th><th>Input</th><th>Output</th></tr></thead><tbody>'
|
||||
mods.forEach((m,i)=>{ h+=`<tr><td><span class="mod-dot" style="background:${cols[i%cols.length]}"></span>${esc(m.module||m.name||'—')}</td><td>${(m.total_tokens||0).toLocaleString('no')}</td><td>${(m.input_tokens||0).toLocaleString('no')}</td><td>${(m.output_tokens||0).toLocaleString('no')}</td></tr>` })
|
||||
h+='</tbody></table>'
|
||||
h+=`<div style="display:flex;justify-content:center;margin-top:28px">
|
||||
<canvas id="mod-donut" width="260" height="260"></canvas>
|
||||
</div>`
|
||||
el.innerHTML = h
|
||||
renderDonut(mods, cols)
|
||||
return
|
||||
}
|
||||
el.innerHTML=h
|
||||
}catch(e){ el.innerHTML=`<div class="empty-state"><div class="empty-icon">❌</div><div class="empty-title">Feil</div><div class="empty-desc">${e.message}</div></div>` }
|
||||
}
|
||||
|
||||
function renderDonut(mods, cols) {
|
||||
const canvas = document.getElementById('mod-donut')
|
||||
if (!canvas) return
|
||||
if (canvas._chart) canvas._chart.destroy()
|
||||
canvas._chart = new Chart(canvas, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: mods.map(m => m.module || m.name || '—'),
|
||||
datasets: [{
|
||||
data: mods.map(m => m.total_tokens || 0),
|
||||
backgroundColor: mods.map((_, i) => cols[i % cols.length]),
|
||||
borderColor: '#0a0a0f',
|
||||
borderWidth: 3,
|
||||
hoverOffset: 6,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
cutout: '68%',
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
color: '#8b909c',
|
||||
font: { size: 11, family: '-apple-system,BlinkMacSystemFont,Inter,system-ui,sans-serif' },
|
||||
padding: 14,
|
||||
boxWidth: 10,
|
||||
boxHeight: 10,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: ctx => ` ${ctx.label}: ${ctx.parsed.toLocaleString('no')} tokens`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BUILD PANEL
|
||||
async function loadBuild(){
|
||||
const el=document.getElementById('build-body')
|
||||
|
|
@ -707,20 +754,14 @@ function mdShowGrid(){
|
|||
|
||||
function renderMd(text){
|
||||
let s=esc(text)
|
||||
// code blocks first
|
||||
s=s.replace(/```[^\n]*\n([\s\S]*?)```/g,'<pre><code>$1</code></pre>')
|
||||
s=s.replace(/`([^`]+)`/g,'<code>$1</code>')
|
||||
// headings
|
||||
s=s.replace(/^### (.+)$/gm,'<h3>$1</h3>')
|
||||
s=s.replace(/^## (.+)$/gm,'<h2>$1</h2>')
|
||||
s=s.replace(/^# (.+)$/gm,'<h1>$1</h1>')
|
||||
// bold
|
||||
s=s.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
|
||||
// hr
|
||||
s=s.replace(/^---$/gm,'<hr>')
|
||||
// blockquote
|
||||
s=s.replace(/^> (.+)$/gm,'<blockquote>$1</blockquote>')
|
||||
// tables (simple)
|
||||
s=s.replace(/((?:^\|.+\|\n)+)/gm, tbl=>{
|
||||
const rows=tbl.trim().split('\n').filter(r=>!/^\|[-| :]+\|$/.test(r))
|
||||
if(!rows.length) return tbl
|
||||
|
|
@ -734,11 +775,9 @@ function renderMd(text){
|
|||
out+='</tbody></table>'
|
||||
return out
|
||||
})
|
||||
// lists
|
||||
s=s.replace(/^[-*] (.+)$/gm,'<li>$1</li>')
|
||||
s=s.replace(/(<li>.*<\/li>)/s, m=>'<ul>'+m+'</ul>')
|
||||
s=s.replace(/^\d+\. (.+)$/gm,'<li>$1</li>')
|
||||
// paragraphs — wrap loose lines
|
||||
s=s.replace(/^(?!<[htublp|]).+$/gm, line=>line.trim()?`<p>${line}</p>`:'')
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user