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 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}
|
.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>
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
|
|
@ -593,16 +594,62 @@ async function loadBilling(){
|
||||||
h+='</div>'
|
h+='</div>'
|
||||||
}else{ h+='<div class="sec-title" style="color:var(--green)">✅ Ingen anomalier</div>' }
|
}else{ h+='<div class="sec-title" style="color:var(--green)">✅ Ingen anomalier</div>' }
|
||||||
const mods = mod?.modules||mod?.data||[]
|
const mods = mod?.modules||mod?.data||[]
|
||||||
|
const cols = ['#7c5cff','#00d4a8','#ffb547','#ff5c7c','#3ecf8e','#5c8fff','#ff9f7c','#c084fc']
|
||||||
if(mods.length){
|
if(mods.length){
|
||||||
const cols=['#7c5cff','#00d4a8','#ffb547','#ff5c7c','#3ecf8e']
|
h+='<div class="sec-title" style="margin-top:24px">Token-forbruk per modul</div>'
|
||||||
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+='<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>` })
|
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+='</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
|
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>` }
|
}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
|
// BUILD PANEL
|
||||||
async function loadBuild(){
|
async function loadBuild(){
|
||||||
const el=document.getElementById('build-body')
|
const el=document.getElementById('build-body')
|
||||||
|
|
@ -707,20 +754,14 @@ function mdShowGrid(){
|
||||||
|
|
||||||
function renderMd(text){
|
function renderMd(text){
|
||||||
let s=esc(text)
|
let s=esc(text)
|
||||||
// code blocks first
|
|
||||||
s=s.replace(/```[^\n]*\n([\s\S]*?)```/g,'<pre><code>$1</code></pre>')
|
s=s.replace(/```[^\n]*\n([\s\S]*?)```/g,'<pre><code>$1</code></pre>')
|
||||||
s=s.replace(/`([^`]+)`/g,'<code>$1</code>')
|
s=s.replace(/`([^`]+)`/g,'<code>$1</code>')
|
||||||
// headings
|
|
||||||
s=s.replace(/^### (.+)$/gm,'<h3>$1</h3>')
|
s=s.replace(/^### (.+)$/gm,'<h3>$1</h3>')
|
||||||
s=s.replace(/^## (.+)$/gm,'<h2>$1</h2>')
|
s=s.replace(/^## (.+)$/gm,'<h2>$1</h2>')
|
||||||
s=s.replace(/^# (.+)$/gm,'<h1>$1</h1>')
|
s=s.replace(/^# (.+)$/gm,'<h1>$1</h1>')
|
||||||
// bold
|
|
||||||
s=s.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
|
s=s.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
|
||||||
// hr
|
|
||||||
s=s.replace(/^---$/gm,'<hr>')
|
s=s.replace(/^---$/gm,'<hr>')
|
||||||
// blockquote
|
|
||||||
s=s.replace(/^> (.+)$/gm,'<blockquote>$1</blockquote>')
|
s=s.replace(/^> (.+)$/gm,'<blockquote>$1</blockquote>')
|
||||||
// tables (simple)
|
|
||||||
s=s.replace(/((?:^\|.+\|\n)+)/gm, tbl=>{
|
s=s.replace(/((?:^\|.+\|\n)+)/gm, tbl=>{
|
||||||
const rows=tbl.trim().split('\n').filter(r=>!/^\|[-| :]+\|$/.test(r))
|
const rows=tbl.trim().split('\n').filter(r=>!/^\|[-| :]+\|$/.test(r))
|
||||||
if(!rows.length) return tbl
|
if(!rows.length) return tbl
|
||||||
|
|
@ -734,11 +775,9 @@ function renderMd(text){
|
||||||
out+='</tbody></table>'
|
out+='</tbody></table>'
|
||||||
return out
|
return out
|
||||||
})
|
})
|
||||||
// lists
|
|
||||||
s=s.replace(/^[-*] (.+)$/gm,'<li>$1</li>')
|
s=s.replace(/^[-*] (.+)$/gm,'<li>$1</li>')
|
||||||
s=s.replace(/(<li>.*<\/li>)/s, m=>'<ul>'+m+'</ul>')
|
s=s.replace(/(<li>.*<\/li>)/s, m=>'<ul>'+m+'</ul>')
|
||||||
s=s.replace(/^\d+\. (.+)$/gm,'<li>$1</li>')
|
s=s.replace(/^\d+\. (.+)$/gm,'<li>$1</li>')
|
||||||
// paragraphs — wrap loose lines
|
|
||||||
s=s.replace(/^(?!<[htublp|]).+$/gm, line=>line.trim()?`<p>${line}</p>`:'')
|
s=s.replace(/^(?!<[htublp|]).+$/gm, line=>line.trim()?`<p>${line}</p>`:'')
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user