feat(booterm): structured pty_exited WS notifications. Plan-validated, impl-validated, code-reviewed green (contracts build clean, contracts test 29/29, booterm + web typecheck clean). wip: in-progress inference/provider refactor (agents.ts, provider.ts, new llama-providers.ts, removed llama-args-validator), plus arena, dispatcher, compaction, schema changes. openspec: pty-exit-notifications complete; x-agent-flags planned (not yet implemented).
108 lines
2.9 KiB
TypeScript
108 lines
2.9 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import * as echarts from 'echarts/core';
|
|
import { GaugeChart } from 'echarts/charts';
|
|
import { CanvasRenderer } from 'echarts/renderers';
|
|
import type { EChartsType } from 'echarts/core';
|
|
import { buildEChartsTheme } from './buildEChartsTheme';
|
|
|
|
echarts.use([GaugeChart, CanvasRenderer]);
|
|
|
|
interface VramGaugeProps {
|
|
used: number; // MB
|
|
total: number; // MB
|
|
size?: number;
|
|
}
|
|
|
|
export function VramGauge({ used, total, size = 120 }: VramGaugeProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const chartRef = useRef<EChartsType | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
if (!chartRef.current) {
|
|
const theme = buildEChartsTheme();
|
|
chartRef.current = echarts.init(containerRef.current, theme);
|
|
}
|
|
|
|
const chart = chartRef.current;
|
|
const root = getComputedStyle(document.documentElement);
|
|
const get = (prop: string) => root.getPropertyValue(prop).trim();
|
|
|
|
const pct = total > 0 ? Math.round((used / total) * 100) : 0;
|
|
|
|
// Derive gauge progress color from CSS custom properties
|
|
// Green -> Amber -> Red as utilization increases
|
|
let color = get('--glow-green');
|
|
if (pct > 80) color = get('--glow-red');
|
|
else if (pct > 60) color = get('--glow-amber');
|
|
|
|
chart.setOption({
|
|
backgroundColor: 'transparent',
|
|
series: [
|
|
{
|
|
type: 'gauge',
|
|
startAngle: 220,
|
|
endAngle: -40,
|
|
min: 0,
|
|
max: total,
|
|
radius: '90%',
|
|
center: ['50%', '55%'],
|
|
pointer: { show: false },
|
|
progress: {
|
|
show: true,
|
|
overlap: false,
|
|
roundCap: true,
|
|
clip: false,
|
|
itemStyle: { color },
|
|
width: 8,
|
|
},
|
|
axisLine: {
|
|
lineStyle: {
|
|
width: 8,
|
|
color: [[1, get('--border')]],
|
|
},
|
|
},
|
|
axisTick: { show: false },
|
|
splitLine: { show: false },
|
|
axisLabel: { show: false },
|
|
title: {
|
|
show: true,
|
|
offsetCenter: ['0%', '-10%'],
|
|
fontSize: 11,
|
|
color: get('--muted-foreground'),
|
|
fontFamily: 'Inter',
|
|
},
|
|
detail: {
|
|
show: true,
|
|
offsetCenter: ['0%', '10%'],
|
|
fontSize: 18,
|
|
fontWeight: 'bold',
|
|
color: get('--foreground'),
|
|
fontFamily: 'Orbitron',
|
|
formatter: () => `${used} / ${total} MB`,
|
|
},
|
|
data: [{ value: used, name: 'VRAM' }],
|
|
},
|
|
],
|
|
});
|
|
|
|
const observer = new ResizeObserver(() => chart.resize());
|
|
observer.observe(containerRef.current);
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
chart.dispose();
|
|
chartRef.current = null;
|
|
};
|
|
}, [used, total]);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="flex items-center justify-center"
|
|
style={{ width: size, height: size }}
|
|
/>
|
|
);
|
|
}
|