Performance-Panel mit Kosten-Tracker und Statistiken
- Neues PerformancePanel.svelte mit:
- Kosten-Übersicht (Session, Heute, Gesamt)
- Token-Statistiken mit Input/Output Ratio-Balken
- Latenz-Verteilung (Min, P50, P95, Max)
- Fehlerrate-Anzeige
- Letzte Sessions Übersicht
- Neuer Tab "📈 Kosten" im mittleren Panel
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b75c61faf4
commit
6b8f28145f
2 changed files with 471 additions and 0 deletions
467
src/lib/components/PerformancePanel.svelte
Normal file
467
src/lib/components/PerformancePanel.svelte
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
<script lang="ts">
|
||||
// Performance-Panel — Kosten, Token, Latenz-Statistiken
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { onMount } from 'svelte';
|
||||
import { sessionStats, monitorStats, monitorEvents } from '$lib/stores/app';
|
||||
|
||||
// Aggregierte Statistiken
|
||||
interface AggregatedStats {
|
||||
totalSessions: number;
|
||||
totalCost: number;
|
||||
totalTokensIn: number;
|
||||
totalTokensOut: number;
|
||||
totalMessages: number;
|
||||
todayCost: number;
|
||||
todaySessions: number;
|
||||
}
|
||||
|
||||
interface SessionSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
cost_usd: number;
|
||||
token_input: number;
|
||||
token_output: number;
|
||||
message_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
let aggregated = $state<AggregatedStats>({
|
||||
totalSessions: 0,
|
||||
totalCost: 0,
|
||||
totalTokensIn: 0,
|
||||
totalTokensOut: 0,
|
||||
totalMessages: 0,
|
||||
todayCost: 0,
|
||||
todaySessions: 0,
|
||||
});
|
||||
|
||||
let recentSessions = $state<SessionSummary[]>([]);
|
||||
let loading = $state(true);
|
||||
|
||||
// Latenz-Verteilung aus Monitor-Events berechnen
|
||||
let latencyStats = $derived(() => {
|
||||
const apiEvents = $monitorEvents.filter(e => e.type === 'api' && e.durationMs);
|
||||
if (apiEvents.length === 0) return null;
|
||||
|
||||
const durations = apiEvents.map(e => e.durationMs!).sort((a, b) => a - b);
|
||||
const sum = durations.reduce((a, b) => a + b, 0);
|
||||
|
||||
return {
|
||||
count: durations.length,
|
||||
min: durations[0],
|
||||
max: durations[durations.length - 1],
|
||||
avg: Math.round(sum / durations.length),
|
||||
p50: durations[Math.floor(durations.length * 0.5)],
|
||||
p95: durations[Math.floor(durations.length * 0.95)] || durations[durations.length - 1],
|
||||
};
|
||||
});
|
||||
|
||||
// Fehlerrate berechnen
|
||||
let errorRate = $derived(() => {
|
||||
const total = $monitorEvents.length;
|
||||
if (total === 0) return 0;
|
||||
const errors = $monitorEvents.filter(e => e.type === 'error').length;
|
||||
return Math.round((errors / total) * 100);
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
await loadStats();
|
||||
});
|
||||
|
||||
async function loadStats() {
|
||||
loading = true;
|
||||
try {
|
||||
// Sessions aus DB laden
|
||||
const sessions = await invoke<SessionSummary[]>('list_sessions', { limit: 100 });
|
||||
|
||||
// Heute-Datum für Filter
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Aggregieren
|
||||
let totalCost = 0;
|
||||
let totalTokensIn = 0;
|
||||
let totalTokensOut = 0;
|
||||
let totalMessages = 0;
|
||||
let todayCost = 0;
|
||||
let todaySessions = 0;
|
||||
|
||||
for (const s of sessions) {
|
||||
totalCost += s.cost_usd || 0;
|
||||
totalTokensIn += s.token_input || 0;
|
||||
totalTokensOut += s.token_output || 0;
|
||||
totalMessages += s.message_count || 0;
|
||||
|
||||
if (s.created_at?.startsWith(today)) {
|
||||
todayCost += s.cost_usd || 0;
|
||||
todaySessions++;
|
||||
}
|
||||
}
|
||||
|
||||
aggregated = {
|
||||
totalSessions: sessions.length,
|
||||
totalCost,
|
||||
totalTokensIn,
|
||||
totalTokensOut,
|
||||
totalMessages,
|
||||
todayCost,
|
||||
todaySessions,
|
||||
};
|
||||
|
||||
// Letzte 5 Sessions für Übersicht
|
||||
recentSessions = sessions.slice(0, 5);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Statistiken:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Formatierung
|
||||
function formatCost(cost: number): string {
|
||||
if (cost < 0.01) return `$${(cost * 100).toFixed(2)}¢`;
|
||||
return `$${cost.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function formatTokens(tokens: number): string {
|
||||
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`;
|
||||
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;
|
||||
return tokens.toString();
|
||||
}
|
||||
|
||||
function formatLatency(ms: number): string {
|
||||
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
return `${ms}ms`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="performance-panel">
|
||||
<div class="panel-header">
|
||||
<h2>📈 Performance</h2>
|
||||
<button class="refresh-btn" onclick={loadStats} disabled={loading} title="Aktualisieren">
|
||||
{loading ? '⏳' : '🔄'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Lade Statistiken...</div>
|
||||
{:else}
|
||||
<!-- Kosten-Übersicht -->
|
||||
<section class="stats-section">
|
||||
<h3>💰 Kosten</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card accent">
|
||||
<span class="stat-value">{formatCost($sessionStats.totalCost)}</span>
|
||||
<span class="stat-label">Aktuelle Session</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{formatCost(aggregated.todayCost)}</span>
|
||||
<span class="stat-label">Heute ({aggregated.todaySessions} Sessions)</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{formatCost(aggregated.totalCost)}</span>
|
||||
<span class="stat-label">Gesamt ({aggregated.totalSessions} Sessions)</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Token-Statistiken -->
|
||||
<section class="stats-section">
|
||||
<h3>🔢 Token</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{formatTokens($sessionStats.totalTokensIn)}</span>
|
||||
<span class="stat-label">Input (Session)</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{formatTokens($sessionStats.totalTokensOut)}</span>
|
||||
<span class="stat-label">Output (Session)</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{formatTokens(aggregated.totalTokensIn + aggregated.totalTokensOut)}</span>
|
||||
<span class="stat-label">Gesamt (alle)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token Ratio Balken -->
|
||||
{#if $sessionStats.totalTokensIn > 0 || $sessionStats.totalTokensOut > 0}
|
||||
{@const total = $sessionStats.totalTokensIn + $sessionStats.totalTokensOut}
|
||||
{@const inPercent = Math.round(($sessionStats.totalTokensIn / total) * 100)}
|
||||
<div class="ratio-bar">
|
||||
<div class="ratio-segment input" style="width: {inPercent}%">
|
||||
<span>In {inPercent}%</span>
|
||||
</div>
|
||||
<div class="ratio-segment output" style="width: {100 - inPercent}%">
|
||||
<span>Out {100 - inPercent}%</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Performance-Indikatoren -->
|
||||
<section class="stats-section">
|
||||
<h3>⚡ Leistung</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{$monitorStats.apiCalls}</span>
|
||||
<span class="stat-label">API-Calls</span>
|
||||
</div>
|
||||
<div class="stat-card" class:error={errorRate() > 5}>
|
||||
<span class="stat-value">{errorRate()}%</span>
|
||||
<span class="stat-label">Fehlerrate</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{$monitorStats.avgLatencyMs}ms</span>
|
||||
<span class="stat-label">Ø Latenz</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Latenz-Details -->
|
||||
{#if latencyStats()}
|
||||
{@const stats = latencyStats()}
|
||||
<div class="latency-details">
|
||||
<div class="latency-row">
|
||||
<span>Min:</span>
|
||||
<span class="value">{formatLatency(stats.min)}</span>
|
||||
</div>
|
||||
<div class="latency-row">
|
||||
<span>P50:</span>
|
||||
<span class="value">{formatLatency(stats.p50)}</span>
|
||||
</div>
|
||||
<div class="latency-row">
|
||||
<span>P95:</span>
|
||||
<span class="value highlight">{formatLatency(stats.p95)}</span>
|
||||
</div>
|
||||
<div class="latency-row">
|
||||
<span>Max:</span>
|
||||
<span class="value">{formatLatency(stats.max)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Letzte Sessions -->
|
||||
{#if recentSessions.length > 1}
|
||||
<section class="stats-section">
|
||||
<h3>📋 Letzte Sessions</h3>
|
||||
<div class="session-list">
|
||||
{#each recentSessions as session}
|
||||
<div class="session-row">
|
||||
<span class="session-title" title={session.title}>
|
||||
{session.title.length > 25 ? session.title.substring(0, 25) + '...' : session.title}
|
||||
</span>
|
||||
<span class="session-cost">{formatCost(session.cost_usd)}</span>
|
||||
<span class="session-tokens">{formatTokens(session.token_input + session.token_output)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.performance-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
font-size: 0.75rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-lg);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.stats-section h3 {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 var(--spacing-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card.accent {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.stat-card.error {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-card.accent .stat-value {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-card.error .stat-value {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Token Ratio Bar */
|
||||
.ratio-bar {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.ratio-segment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.ratio-segment.input {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.ratio-segment.output {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.ratio-segment span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Latenz Details */
|
||||
.latency-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--spacing-xs);
|
||||
margin-top: var(--spacing-xs);
|
||||
padding: var(--spacing-xs);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.latency-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.latency-row span:first-child {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.latency-row .value {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.latency-row .value.highlight {
|
||||
color: #eab308;
|
||||
}
|
||||
|
||||
/* Session Liste */
|
||||
.session-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.session-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.session-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-cost {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.session-tokens {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
||||
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
|
||||
import MonitorPanel from '$lib/components/MonitorPanel.svelte';
|
||||
import PerformancePanel from '$lib/components/PerformancePanel.svelte';
|
||||
|
||||
let activeMiddleTab = 'activity';
|
||||
let activeRightTab = 'agents';
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
const middleTabs = [
|
||||
{ id: 'activity', label: 'Aktivität', icon: '📋' },
|
||||
{ id: 'monitor', label: 'Monitor', icon: '📊' },
|
||||
{ id: 'perf', label: 'Kosten', icon: '📈' },
|
||||
{ id: 'knowledge', label: 'Wissen', icon: '📚' },
|
||||
{ id: 'memory', label: 'Gedächtnis', icon: '🧠' },
|
||||
{ id: 'audit', label: 'Historie', icon: '📝' },
|
||||
|
|
@ -68,6 +70,8 @@
|
|||
<ActivityPanel />
|
||||
{:else if activeMiddleTab === 'monitor'}
|
||||
<MonitorPanel />
|
||||
{:else if activeMiddleTab === 'perf'}
|
||||
<PerformancePanel />
|
||||
{:else if activeMiddleTab === 'knowledge'}
|
||||
<KnowledgePanel />
|
||||
{:else if activeMiddleTab === 'memory'}
|
||||
|
|
|
|||
Loading…
Reference in a new issue