refactor: remove Diagnostics component and related LLM call telemetry
This commit is contained in:
@@ -1,162 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { RefreshCw, AlertCircle, CheckCircle2 } from 'lucide-react';
|
|
||||||
import Card from '../ui/Card';
|
|
||||||
import Button from '../ui/Button';
|
|
||||||
import Tag from '../ui/Tag';
|
|
||||||
import * as db from '../../lib/db';
|
|
||||||
|
|
||||||
// Public Anthropic pricing per 1M tokens. Update manually when prices change.
|
|
||||||
const PRICES = {
|
|
||||||
'claude-haiku-4-5-20251001': { input: 1.0, output: 5.0, cache_read: 0.10, cache_write: 1.25 },
|
|
||||||
'claude-haiku-4-5': { input: 1.0, output: 5.0, cache_read: 0.10, cache_write: 1.25 },
|
|
||||||
'claude-sonnet-4-6': { input: 3.0, output: 15.0, cache_read: 0.30, cache_write: 3.75 },
|
|
||||||
'claude-opus-4-7': { input: 15.0, output: 75.0, cache_read: 1.50, cache_write: 18.75 },
|
|
||||||
};
|
|
||||||
|
|
||||||
function pricesFor(model) {
|
|
||||||
if (!model) return null;
|
|
||||||
if (PRICES[model]) return PRICES[model];
|
|
||||||
if (model.includes('haiku')) return PRICES['claude-haiku-4-5'];
|
|
||||||
if (model.includes('sonnet')) return PRICES['claude-sonnet-4-6'];
|
|
||||||
if (model.includes('opus')) return PRICES['claude-opus-4-7'];
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function costUsd(row) {
|
|
||||||
const p = pricesFor(row.model);
|
|
||||||
if (!p) return null;
|
|
||||||
const inTok = (row.input_tokens || 0) - (row.cache_read_tokens || 0) - (row.cache_create_tokens || 0);
|
|
||||||
const out = row.output_tokens || 0;
|
|
||||||
const cr = row.cache_read_tokens || 0;
|
|
||||||
const cc = row.cache_create_tokens || 0;
|
|
||||||
const usd = (Math.max(inTok, 0) * p.input + out * p.output + cr * p.cache_read + cc * p.cache_write) / 1_000_000;
|
|
||||||
return usd;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtUsd(n) {
|
|
||||||
if (n == null) return '—';
|
|
||||||
if (n < 0.0001) return '<$0.0001';
|
|
||||||
return `$${n.toFixed(4)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtMs(n) {
|
|
||||||
if (n == null) return '—';
|
|
||||||
if (n < 1000) return `${Math.round(n)}ms`;
|
|
||||||
return `${(n / 1000).toFixed(1)}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Diagnostics = () => {
|
|
||||||
const [rows, setRows] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const load = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const r = await db.getRecentLlmCalls(100);
|
|
||||||
setRows(r);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => { load(); }, []);
|
|
||||||
|
|
||||||
const totals = rows.reduce((acc, r) => {
|
|
||||||
acc.input += r.input_tokens || 0;
|
|
||||||
acc.output += r.output_tokens || 0;
|
|
||||||
acc.cacheRead += r.cache_read_tokens || 0;
|
|
||||||
acc.cacheCreate += r.cache_create_tokens || 0;
|
|
||||||
const c = costUsd(r);
|
|
||||||
if (c != null) acc.cost += c;
|
|
||||||
acc.duration += r.duration_ms || 0;
|
|
||||||
if (r.ok) acc.ok++;
|
|
||||||
else acc.fail++;
|
|
||||||
return acc;
|
|
||||||
}, { input: 0, output: 0, cacheRead: 0, cacheCreate: 0, cost: 0, duration: 0, ok: 0, fail: 0 });
|
|
||||||
|
|
||||||
const cacheHitRate = totals.input > 0
|
|
||||||
? Math.round((totals.cacheRead / totals.input) * 100)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<p className="text-fg-muted text-sm">
|
|
||||||
Laatste 100 LLM-aanroepen. Kosten worden lokaal berekend met publieke Anthropic-prijzen (handmatig verversen).
|
|
||||||
</p>
|
|
||||||
<Button onClick={load} disabled={loading}>
|
|
||||||
<RefreshCw size={16} className={`mr-2 ${loading ? 'animate-spin' : ''}`} /> Vernieuwen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
|
||||||
<Card className="p-4 border border-bg-warm">
|
|
||||||
<p className="text-xs text-fg-muted uppercase">Calls</p>
|
|
||||||
<p className="text-2xl font-bold mt-1">{rows.length}</p>
|
|
||||||
<p className="text-xs text-fg-muted mt-1">{totals.ok} ok · {totals.fail} fail</p>
|
|
||||||
</Card>
|
|
||||||
<Card className="p-4 border border-bg-warm">
|
|
||||||
<p className="text-xs text-fg-muted uppercase">Tokens (in/out)</p>
|
|
||||||
<p className="text-2xl font-bold mt-1">{totals.input.toLocaleString()} / {totals.output.toLocaleString()}</p>
|
|
||||||
<p className="text-xs text-fg-muted mt-1">cache read: {totals.cacheRead.toLocaleString()} ({cacheHitRate}%)</p>
|
|
||||||
</Card>
|
|
||||||
<Card className="p-4 border border-bg-warm">
|
|
||||||
<p className="text-xs text-fg-muted uppercase">Geschatte kosten</p>
|
|
||||||
<p className="text-2xl font-bold mt-1">{fmtUsd(totals.cost)}</p>
|
|
||||||
<p className="text-xs text-fg-muted mt-1">over deze 100 aanroepen</p>
|
|
||||||
</Card>
|
|
||||||
<Card className="p-4 border border-bg-warm">
|
|
||||||
<p className="text-xs text-fg-muted uppercase">Gem. duur</p>
|
|
||||||
<p className="text-2xl font-bold mt-1">{rows.length ? fmtMs(totals.duration / rows.length) : '—'}</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="p-0 border border-bg-warm overflow-hidden">
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-bg-warm/40 text-fg-muted text-xs uppercase">
|
|
||||||
<tr>
|
|
||||||
<th className="text-left p-3">Tijd</th>
|
|
||||||
<th className="text-left p-3">Task</th>
|
|
||||||
<th className="text-left p-3">Tier</th>
|
|
||||||
<th className="text-left p-3">Model</th>
|
|
||||||
<th className="text-right p-3">In</th>
|
|
||||||
<th className="text-right p-3">Out</th>
|
|
||||||
<th className="text-right p-3">Cache</th>
|
|
||||||
<th className="text-right p-3">$</th>
|
|
||||||
<th className="text-right p-3">Duur</th>
|
|
||||||
<th className="text-left p-3">Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-bg-warm">
|
|
||||||
{rows.length === 0 ? (
|
|
||||||
<tr><td colSpan={10} className="p-8 text-center text-fg-muted">Nog geen aanroepen geregistreerd.</td></tr>
|
|
||||||
) : rows.map(r => (
|
|
||||||
<tr key={r.id} className="hover:bg-bg-warm/20">
|
|
||||||
<td className="p-3 text-xs text-fg-muted whitespace-nowrap">
|
|
||||||
{r.created ? new Date(r.created).toLocaleString() : '—'}
|
|
||||||
</td>
|
|
||||||
<td className="p-3 font-mono text-xs">{r.task || '—'}</td>
|
|
||||||
<td className="p-3 text-xs">{r.tier || '—'}</td>
|
|
||||||
<td className="p-3 font-mono text-xs">{r.model || '—'}</td>
|
|
||||||
<td className="p-3 text-right">{(r.input_tokens || 0).toLocaleString()}</td>
|
|
||||||
<td className="p-3 text-right">{(r.output_tokens || 0).toLocaleString()}</td>
|
|
||||||
<td className="p-3 text-right text-fg-muted">{(r.cache_read_tokens || 0).toLocaleString()}</td>
|
|
||||||
<td className="p-3 text-right">{fmtUsd(costUsd(r))}</td>
|
|
||||||
<td className="p-3 text-right">{fmtMs(r.duration_ms)}</td>
|
|
||||||
<td className="p-3">
|
|
||||||
{r.ok
|
|
||||||
? <Tag variant="success" className="flex items-center gap-1 w-fit"><CheckCircle2 size={12}/> ok</Tag>
|
|
||||||
: <Tag variant="dark" className="bg-red-100 text-red-800 flex items-center gap-1 w-fit"><AlertCircle size={12}/> {(r.error_msg || r.stop_reason || 'fail').slice(0, 24)}</Tag>}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Diagnostics;
|
|
||||||
@@ -299,17 +299,6 @@ export async function bulkSetCurriculum(year, weeks) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LLM Call Telemetry ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export async function getRecentLlmCalls(limit = 100) {
|
|
||||||
try {
|
|
||||||
const r = await pb.collection('llm_calls').getList(1, limit, { sort: '-created' });
|
|
||||||
return r.items;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Reset for Smoke Testing ──────────────────────────────────────────────────
|
// ── Reset for Smoke Testing ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Database, FileText, Settings, Users, Network, Clock, CheckCircle2, AlertCircle, Save, Info, Layers, CheckSquare, CalendarDays, Activity, RefreshCw, AlertTriangle } from 'lucide-react';
|
import { Database, FileText, Settings, Users, Network, Clock, CheckCircle2, AlertCircle, Save, Info, Layers, CheckSquare, CalendarDays, RefreshCw, AlertTriangle } from 'lucide-react';
|
||||||
import Card from '../../components/ui/Card';
|
import Card from '../../components/ui/Card';
|
||||||
import Tag from '../../components/ui/Tag';
|
import Tag from '../../components/ui/Tag';
|
||||||
import Button from '../../components/ui/Button';
|
import Button from '../../components/ui/Button';
|
||||||
@@ -12,7 +12,6 @@ import ContentManager from '../../components/admin/ContentManager';
|
|||||||
import TestManager from '../../components/admin/TestManager';
|
import TestManager from '../../components/admin/TestManager';
|
||||||
import TeamManager from '../../components/admin/TeamManager';
|
import TeamManager from '../../components/admin/TeamManager';
|
||||||
import CurriculumManager from '../../components/admin/CurriculumManager';
|
import CurriculumManager from '../../components/admin/CurriculumManager';
|
||||||
import Diagnostics from '../../components/admin/Diagnostics';
|
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
const TIER_PLACEHOLDERS = {
|
const TIER_PLACEHOLDERS = {
|
||||||
@@ -93,7 +92,6 @@ const Admin = () => {
|
|||||||
{ key: 'curriculum', icon: CalendarDays, label: 'Curriculum' },
|
{ key: 'curriculum', icon: CalendarDays, label: 'Curriculum' },
|
||||||
{ key: 'graph', icon: Network, label: 'Graph' },
|
{ key: 'graph', icon: Network, label: 'Graph' },
|
||||||
{ key: 'team', icon: Users, label: 'Team' },
|
{ key: 'team', icon: Users, label: 'Team' },
|
||||||
{ key: 'diagnostics', icon: Activity, label: 'Diagnostics' },
|
|
||||||
{ key: 'settings', icon: Settings, label: 'Settings', bottom: true },
|
{ key: 'settings', icon: Settings, label: 'Settings', bottom: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -204,14 +202,6 @@ const Admin = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'diagnostics' && (
|
|
||||||
<div className="animate-in fade-in duration-300 max-w-6xl mx-auto">
|
|
||||||
<h1 className="text-3xl text-teal mb-2">Diagnostics</h1>
|
|
||||||
<p className="text-fg-muted mb-8">LLM-aanroepen, tokenverbruik en geschatte kosten.</p>
|
|
||||||
<Diagnostics />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'settings' && (
|
{activeTab === 'settings' && (
|
||||||
<div className="animate-in fade-in duration-300 max-w-2xl">
|
<div className="animate-in fade-in duration-300 max-w-2xl">
|
||||||
<h1 className="text-3xl text-teal mb-2">Settings</h1>
|
<h1 className="text-3xl text-teal mb-2">Settings</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user