mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 19:02:16 +00:00
274 lines
9.6 KiB
TypeScript
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);
|
|
}
|