Files
OpenJarvis/frontend/src/components/Chat/SystemPanel.tsx
T

274 lines
9.6 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import {
Zap,
Activity,
Thermometer,
DollarSign,
TrendingDown,
Cloud,
HardDrive,
Hash,
X,
} from 'lucide-react';
import { useAppStore } from '../../lib/store';
interface EnergyData {
total_energy_j?: number;
energy_per_token_j?: number;
avg_power_w?: number;
}
interface TelemetryStats {
total_requests?: number;
total_tokens?: number;
}
const CLOUD_PRICING = [
{ name: 'GPT-5.3', input: 2.00, output: 10.00, primary: true },
{ name: 'Claude Opus 4.6', input: 5.00, output: 25.00, primary: false },
{ name: 'Gemini 3.1 Pro', input: 2.00, output: 12.00, primary: false },
];
export function SystemPanel() {
const savings = useAppStore((s) => s.savings);
const toggleSystemPanel = useAppStore((s) => s.toggleSystemPanel);
const [energy, setEnergy] = useState<EnergyData | null>(null);
const [telemetry, setTelemetry] = useState<TelemetryStats | null>(null);
const fetchData = useCallback(async () => {
try {
const base = import.meta.env.VITE_API_URL || '';
const [energyRes, telRes] = await Promise.allSettled([
fetch(`${base}/v1/telemetry/energy`).then((r) => (r.ok ? r.json() : null)),
fetch(`${base}/v1/telemetry/stats`).then((r) => (r.ok ? r.json() : null)),
]);
if (energyRes.status === 'fulfilled' && energyRes.value) {
setEnergy(energyRes.value as EnergyData);
}
if (telRes.status === 'fulfilled' && telRes.value) {
setTelemetry(telRes.value as TelemetryStats);
}
} catch {
// best-effort
}
}, []);
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 3000);
return () => clearInterval(interval);
}, [fetchData]);
// Re-fetch energy/telemetry when savings updates (after a chat message)
useEffect(() => {
if (savings) fetchData();
}, [savings, fetchData]);
const thermalStatus =
(energy?.avg_power_w ?? 0) < 50
? { label: 'Cool', color: 'var(--color-success)' }
: (energy?.avg_power_w ?? 0) < 150
? { label: 'Warm', color: 'var(--color-warning)' }
: { label: 'Hot', color: 'var(--color-error)' };
const promptK = (savings?.total_prompt_tokens ?? 0) / 1000;
const completionK = (savings?.total_completion_tokens ?? 0) / 1000;
return (
<div
className="flex flex-col h-full overflow-y-auto"
style={{
width: 280,
minWidth: 280,
background: 'var(--color-bg)',
borderLeft: '1px solid var(--color-border)',
}}
>
{/* Header */}
<div
className="flex items-center justify-between px-4 py-3 shrink-0"
style={{ borderBottom: '1px solid var(--color-border)' }}
>
<span className="text-xs font-semibold tracking-wide uppercase" style={{ color: 'var(--color-text-secondary)' }}>
System
</span>
<button
onClick={toggleSystemPanel}
className="p-1 rounded-md transition-colors cursor-pointer"
style={{ color: 'var(--color-text-tertiary)' }}
title="Close panel"
>
<X size={14} />
</button>
</div>
<div className="flex flex-col gap-4 p-4">
{/* Session Stats */}
<section>
<h4 className="text-[11px] font-medium uppercase tracking-wide mb-2" style={{ color: 'var(--color-text-tertiary)' }}>
Session
</h4>
<div className="grid grid-cols-2 gap-2">
<MiniStat icon={Hash} label="Requests" value={String(telemetry?.total_requests ?? savings?.total_calls ?? 0)} />
<MiniStat icon={Activity} label="Tokens" value={formatNumber(telemetry?.total_tokens ?? savings?.total_tokens ?? 0)} />
</div>
</section>
{/* Energy */}
<section>
<h4 className="text-[11px] font-medium uppercase tracking-wide mb-2" style={{ color: 'var(--color-text-tertiary)' }}>
Energy
</h4>
<div className="grid grid-cols-2 gap-2">
<MiniStat
icon={Zap}
label="Total"
value={((energy?.total_energy_j ?? 0) / 1000).toFixed(1)}
unit="kJ"
/>
<MiniStat
icon={Activity}
label="Per Token"
value={(energy?.energy_per_token_j ?? 0).toFixed(3)}
unit="J"
/>
<MiniStat
icon={Thermometer}
label="Avg Power"
value={(energy?.avg_power_w ?? 0).toFixed(1)}
unit="W"
/>
<div
className="flex items-center gap-1.5 rounded-lg px-2.5 py-2"
style={{ background: 'var(--color-bg-secondary)', border: '1px solid var(--color-border)' }}
>
<span className="w-2 h-2 rounded-full shrink-0" style={{ background: thermalStatus.color }} />
<span className="text-[11px]" style={{ color: 'var(--color-text-secondary)' }}>
{thermalStatus.label}
</span>
</div>
</div>
</section>
{/* Cost Comparison */}
<section>
<h4 className="text-[11px] font-medium uppercase tracking-wide mb-2" style={{ color: 'var(--color-text-tertiary)' }}>
Cost Comparison
</h4>
{/* Local */}
<div
className="flex items-center gap-2 rounded-lg px-3 py-2 mb-2"
style={{ background: 'var(--color-accent-subtle)', border: '1px solid var(--color-accent)' }}
>
<HardDrive size={14} style={{ color: 'var(--color-accent)' }} />
<div className="flex-1 min-w-0">
<div className="text-xs font-medium truncate" style={{ color: 'var(--color-text)' }}>Local</div>
</div>
<div className="text-sm font-semibold" style={{ color: 'var(--color-success)' }}>
${(savings?.local_cost ?? 0).toFixed(4)}
</div>
</div>
{/* Cloud providers */}
<div className="flex flex-col gap-1.5">
{CLOUD_PRICING.map((provider) => {
const cost = (promptK * provider.input) / 1000 + (completionK * provider.output) / 1000;
const saved = cost - (savings?.local_cost ?? 0);
return (
<div
key={provider.name}
className="flex items-center gap-2 rounded-lg px-3 py-2"
style={{
background: provider.primary ? 'var(--color-bg-secondary)' : 'var(--color-bg-secondary)',
border: provider.primary ? '1px solid var(--color-border-accent, var(--color-accent))' : '1px solid transparent',
}}
>
<Cloud size={14} style={{ color: 'var(--color-text-tertiary)' }} />
<div className="flex-1 min-w-0">
<div
className="text-xs truncate"
style={{
color: provider.primary ? 'var(--color-text)' : 'var(--color-text-secondary)',
fontWeight: provider.primary ? 500 : 400,
}}
>
{provider.name}
</div>
</div>
<div className="text-right shrink-0">
<div className="text-xs font-mono" style={{ color: 'var(--color-text)' }}>
${cost.toFixed(4)}
</div>
{saved > 0.0001 && (
<div className="text-[9px] flex items-center gap-0.5 justify-end" style={{ color: 'var(--color-success)' }}>
<TrendingDown size={8} />
${saved.toFixed(4)}
</div>
)}
</div>
</div>
);
})}
</div>
{/* Server-reported savings */}
{savings && savings.per_provider.length > 0 && (
<div className="mt-2 pt-2" style={{ borderTop: '1px solid var(--color-border)' }}>
<div className="text-[10px] mb-1" style={{ color: 'var(--color-text-tertiary)' }}>
Server-reported
</div>
{savings.per_provider.map((p) => (
<div key={p.provider} className="flex justify-between text-[11px] py-0.5">
<span style={{ color: 'var(--color-text-secondary)' }}>{p.label}</span>
<span className="font-mono" style={{ color: 'var(--color-success)' }}>${p.total_cost.toFixed(4)}</span>
</div>
))}
</div>
)}
</section>
</div>
</div>
);
}
function MiniStat({
icon: Icon,
label,
value,
unit,
}: {
icon: typeof Zap;
label: string;
value: string;
unit?: string;
}) {
return (
<div
className="rounded-lg px-2.5 py-2"
style={{ background: 'var(--color-bg-secondary)', border: '1px solid var(--color-border)' }}
>
<div className="flex items-center gap-1 mb-0.5">
<Icon size={10} style={{ color: 'var(--color-accent)' }} />
<span className="text-[10px]" style={{ color: 'var(--color-text-tertiary)' }}>
{label}
</span>
</div>
<div className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
{value}
{unit && (
<span className="text-[10px] font-normal ml-0.5" style={{ color: 'var(--color-text-tertiary)' }}>
{unit}
</span>
)}
</div>
</div>
);
}
function formatNumber(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
return String(n);
}