diff --git a/static/opax.html b/static/opax.html
index d30edfc..7d6cecb 100644
--- a/static/opax.html
+++ b/static/opax.html
@@ -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}
+
@@ -593,16 +594,62 @@ async function loadBilling(){
h+='
'
}else{ h+='✅ Ingen anomalier
' }
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+='Token-forbruk per modul
| Modul | Tokens | Input | Output |
'
+ h+='Token-forbruk per modul
'
+ h+='| Modul | Tokens | Input | Output |
'
mods.forEach((m,i)=>{ h+=`| ${esc(m.module||m.name||'—')} | ${(m.total_tokens||0).toLocaleString('no')} | ${(m.input_tokens||0).toLocaleString('no')} | ${(m.output_tokens||0).toLocaleString('no')} |
` })
h+='
'
+ h+=`
+
+
`
+ el.innerHTML = h
+ renderDonut(mods, cols)
+ return
}
el.innerHTML=h
}catch(e){ el.innerHTML=`` }
}
+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,'$1
')
s=s.replace(/`([^`]+)`/g,'$1')
- // headings
s=s.replace(/^### (.+)$/gm,'$1
')
s=s.replace(/^## (.+)$/gm,'$1
')
s=s.replace(/^# (.+)$/gm,'$1
')
- // bold
s=s.replace(/\*\*(.+?)\*\*/g,'$1')
- // hr
s=s.replace(/^---$/gm,'
')
- // blockquote
s=s.replace(/^> (.+)$/gm,'$1
')
- // 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+='
'
return out
})
- // lists
s=s.replace(/^[-*] (.+)$/gm,'$1')
s=s.replace(/(.*<\/li>)/s, m=>'')
s=s.replace(/^\d+\. (.+)$/gm,'$1')
- // paragraphs — wrap loose lines
s=s.replace(/^(?!<[htublp|]).+$/gm, line=>line.trim()?`${line}
`:'')
return s
}