feat: add two-column admin panel with LLM analytics display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+178
-6
@@ -1,4 +1,4 @@
|
||||
import type { TunableConstants, TunableKey } from '@dflike/shared';
|
||||
import type { TunableConstants, TunableKey, LlmStatsData } from '@dflike/shared';
|
||||
|
||||
const LABELS: Record<TunableKey, string> = {
|
||||
TICK_RATE: 'Tick Rate (ticks/sec)',
|
||||
@@ -44,6 +44,10 @@ export class AdminPanel {
|
||||
onAuth: ((password: string) => void) | null = null;
|
||||
onUpdate: ((key: TunableKey, value: number) => void) | null = null;
|
||||
onReset: (() => void) | null = null;
|
||||
onRequestLlmStats: (() => void) | null = null;
|
||||
onToggleLlmTracking: ((enabled: boolean) => void) | null = null;
|
||||
private statsColumn: HTMLDivElement | null = null;
|
||||
private trackingToggle: HTMLInputElement | null = null;
|
||||
|
||||
constructor() {
|
||||
this.container = document.createElement('div');
|
||||
@@ -118,9 +122,9 @@ export class AdminPanel {
|
||||
this.editorView = document.createElement('div');
|
||||
this.editorView.style.cssText = `
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
overflow-y: auto;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
@@ -152,10 +156,130 @@ export class AdminPanel {
|
||||
}
|
||||
}
|
||||
|
||||
requestStatsRefresh(): void {
|
||||
if (this.authenticated) {
|
||||
this.onRequestLlmStats?.();
|
||||
}
|
||||
}
|
||||
|
||||
updateLlmStats(data: LlmStatsData): void {
|
||||
if (!this.statsColumn) return;
|
||||
if (this.trackingToggle) {
|
||||
this.trackingToggle.checked = data.trackingEnabled;
|
||||
}
|
||||
|
||||
// Remove everything after the toggle row
|
||||
while (this.statsColumn.children.length > 1) {
|
||||
this.statsColumn.removeChild(this.statsColumn.lastChild!);
|
||||
}
|
||||
|
||||
const fmt = (n: number) => n < 0.01 ? `$${n.toFixed(6)}` : `$${n.toFixed(4)}`;
|
||||
|
||||
if (data.types.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.style.cssText = 'color: #555; font-size: 8px; padding: 8px 0; text-align: center;';
|
||||
empty.textContent = 'No LLM calls recorded yet';
|
||||
this.statsColumn.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const type of data.types) {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #1c1450;
|
||||
`;
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'display: flex; justify-content: space-between; align-items: baseline;';
|
||||
|
||||
const nameEl = document.createElement('span');
|
||||
nameEl.style.cssText = 'color: #e0d0b0; font-size: 9px; font-weight: bold;';
|
||||
nameEl.textContent = type.label;
|
||||
|
||||
const countEl = document.createElement('span');
|
||||
countEl.style.cssText = 'color: #8878a8; font-size: 8px;';
|
||||
countEl.textContent = `${type.count} calls`;
|
||||
|
||||
header.appendChild(nameEl);
|
||||
header.appendChild(countEl);
|
||||
row.appendChild(header);
|
||||
|
||||
const costLine = document.createElement('div');
|
||||
costLine.style.cssText = 'color: #a0a0c0; font-size: 7px;';
|
||||
costLine.textContent = `min ${fmt(type.minCost)} / avg ${fmt(type.avgCost)} / max ${fmt(type.maxCost)}`;
|
||||
row.appendChild(costLine);
|
||||
|
||||
const totalLine = document.createElement('div');
|
||||
totalLine.style.cssText = 'display: flex; justify-content: space-between; font-size: 7px;';
|
||||
|
||||
const totalCostEl = document.createElement('span');
|
||||
totalCostEl.style.cssText = 'color: #a0a0c0;';
|
||||
totalCostEl.textContent = `total: ${fmt(type.totalCost)}`;
|
||||
|
||||
const ratesEl = document.createElement('span');
|
||||
ratesEl.style.cssText = `color: ${type.failPct > 0 ? '#ff6666' : '#a0a0c0'};`;
|
||||
ratesEl.textContent = `retry ${type.retryPct}% / fail ${type.failPct}%`;
|
||||
|
||||
totalLine.appendChild(totalCostEl);
|
||||
totalLine.appendChild(ratesEl);
|
||||
row.appendChild(totalLine);
|
||||
|
||||
this.statsColumn.appendChild(row);
|
||||
}
|
||||
|
||||
// Totals row
|
||||
const totalsRow = document.createElement('div');
|
||||
totalsRow.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
padding: 6px 0;
|
||||
border-top: 2px solid #8878a8;
|
||||
margin-top: 4px;
|
||||
`;
|
||||
|
||||
const totalsHeader = document.createElement('div');
|
||||
totalsHeader.style.cssText = 'display: flex; justify-content: space-between; align-items: baseline;';
|
||||
|
||||
const totalsLabel = document.createElement('span');
|
||||
totalsLabel.style.cssText = 'color: #e0d0b0; font-size: 9px; font-weight: bold;';
|
||||
totalsLabel.textContent = 'TOTAL';
|
||||
|
||||
const totalsCalls = document.createElement('span');
|
||||
totalsCalls.style.cssText = 'color: #8878a8; font-size: 8px;';
|
||||
totalsCalls.textContent = `${data.totalCount} calls`;
|
||||
|
||||
totalsHeader.appendChild(totalsLabel);
|
||||
totalsHeader.appendChild(totalsCalls);
|
||||
totalsRow.appendChild(totalsHeader);
|
||||
|
||||
const totalsCost = document.createElement('div');
|
||||
totalsCost.style.cssText = 'color: #a0a0c0; font-size: 7px;';
|
||||
totalsCost.textContent = `total cost: ${fmt(data.totalCost)} / retries: ${data.totalRetries} / failures: ${data.totalFailures}`;
|
||||
totalsRow.appendChild(totalsCost);
|
||||
|
||||
this.statsColumn.appendChild(totalsRow);
|
||||
}
|
||||
|
||||
private buildEditor(constants: TunableConstants): void {
|
||||
this.editorView.innerHTML = '';
|
||||
this.inputs.clear();
|
||||
|
||||
// Left column: existing constants editor
|
||||
const leftCol = document.createElement('div');
|
||||
leftCol.style.cssText = `
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const debounceTimers = new Map<TunableKey, ReturnType<typeof setTimeout>>();
|
||||
|
||||
for (const key of Object.keys(LABELS) as TunableKey[]) {
|
||||
@@ -219,7 +343,7 @@ export class AdminPanel {
|
||||
|
||||
row.appendChild(labelRow);
|
||||
row.appendChild(inputEl);
|
||||
this.editorView.appendChild(row);
|
||||
leftCol.appendChild(row);
|
||||
}
|
||||
|
||||
// Reset button
|
||||
@@ -239,6 +363,54 @@ export class AdminPanel {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
this.onReset?.();
|
||||
});
|
||||
this.editorView.appendChild(resetBtn);
|
||||
leftCol.appendChild(resetBtn);
|
||||
|
||||
// Right column: LLM analytics
|
||||
const rightCol = document.createElement('div');
|
||||
rightCol.style.cssText = `
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
`;
|
||||
this.statsColumn = rightCol;
|
||||
|
||||
// Tracking toggle
|
||||
const toggleRow = document.createElement('div');
|
||||
toggleRow.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #1c1450;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const toggleLabel = document.createElement('span');
|
||||
toggleLabel.style.cssText = 'color: #a0a0c0; font-size: 8px;';
|
||||
toggleLabel.textContent = 'LLM Tracking';
|
||||
|
||||
this.trackingToggle = document.createElement('input');
|
||||
this.trackingToggle.type = 'checkbox';
|
||||
this.trackingToggle.checked = true;
|
||||
this.trackingToggle.style.cssText = 'accent-color: #8878a8; cursor: pointer;';
|
||||
this.trackingToggle.addEventListener('change', () => {
|
||||
this.onToggleLlmTracking?.(this.trackingToggle!.checked);
|
||||
});
|
||||
|
||||
toggleRow.appendChild(toggleLabel);
|
||||
toggleRow.appendChild(this.trackingToggle);
|
||||
rightCol.appendChild(toggleRow);
|
||||
|
||||
// Stats placeholder
|
||||
const statsPlaceholder = document.createElement('div');
|
||||
statsPlaceholder.style.cssText = 'color: #555; font-size: 8px; padding: 8px 0; text-align: center;';
|
||||
statsPlaceholder.textContent = 'Loading stats...';
|
||||
rightCol.appendChild(statsPlaceholder);
|
||||
|
||||
this.editorView.appendChild(leftCol);
|
||||
this.editorView.appendChild(rightCol);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user