Bonsai-Chat-WebGPU / src /app /components /InspectorPanel.tsx
WaveCut's picture
Add resizable sidebars and inline artifacts
7142bcd verified
Raw
History Blame Contribute Delete
7 kB
import type { ArtifactDocument } from '../../lib/agent-tools';
import type { TelemetrySnapshot, ToolEvent } from '../../lib/contracts';
import { formatBytes, formatPercent } from '../../lib/format';
interface InspectorPanelProps {
hidden?: boolean;
telemetry: TelemetrySnapshot;
toolEvents: ToolEvent[];
artifacts: ArtifactDocument[];
onSelectArtifact: (artifactId: string) => void;
}
export function InspectorPanel({ hidden = false, telemetry, toolEvents, artifacts, onSelectArtifact }: InspectorPanelProps) {
const contextRatio = telemetry.contextLimit > 0 ? telemetry.contextUsed / telemetry.contextLimit : 0;
const prefillRatio = telemetry.promptTotal > 0 ? telemetry.promptProcessed / telemetry.promptTotal : 0;
const activeRate = telemetry.inferencePhase === 'prefill'
? telemetry.prefillTokensPerSecond
: telemetry.decodeTokensPerSecond || telemetry.tokensPerSecond;
const speedRatio = Math.min(1, Math.max(0, activeRate / 50));
const gaugeLabel = telemetry.inferencePhase === 'prefill'
? 'Prefill'
: telemetry.inferencePhase === 'decode'
? 'Decode'
: 'Average';
const samplingLabel = telemetry.inferencePhase === 'idle'
? 'Ready'
: telemetry.inferencePhase === 'complete' ? 'Average' : 'Live';
return (
<aside className="inspector" aria-label="Runtime inspector" hidden={hidden}>
<section className="inspector-section telemetry-section" aria-labelledby="telemetry-title">
<div className="section-heading">
<h2 id="telemetry-title">Telemetry</h2>
<span className={`sampling-label phase-${telemetry.inferencePhase}`}>{samplingLabel}</span>
</div>
<div className="speed-gauge">
<div className="gauge-ring" style={{ '--gauge': `${Math.round(speedRatio * 100)}%` } as React.CSSProperties}>
<div>
<strong>{activeRate.toFixed(1)}</strong>
<small><span>{gaugeLabel}</span><span>tok/s</span></small>
</div>
</div>
<dl>
<div><dt>Backend</dt><dd>{telemetry.backend}</dd></div>
<div><dt>First token</dt><dd>{telemetry.timeToFirstTokenMs} ms</dd></div>
<div><dt>Graph splits</dt><dd>{telemetry.graphSplits ?? '—'}</dd></div>
</dl>
</div>
<div className="telemetry-live-grid" aria-label="Live inference rates">
<TelemetryMetric
label="Prefill"
value={`${telemetry.prefillTokensPerSecond.toFixed(1)} tok/s`}
detail={telemetry.promptTotal > 0
? `${telemetry.promptProcessed.toLocaleString()} / ${telemetry.promptTotal.toLocaleString()} tokens`
: 'No prompt yet'}
ratio={prefillRatio}
active={telemetry.inferencePhase === 'prefill'}
progressLabel={telemetry.promptTotal > 0 ? formatPercent(prefillRatio) : 'Waiting'}
/>
<TelemetryMetric
label="Decode"
value={`${telemetry.decodeTokensPerSecond.toFixed(1)} tok/s`}
detail={`${telemetry.completionTokens.toLocaleString()} generated tokens`}
ratio={telemetry.decodeTokensPerSecond / 50}
active={telemetry.inferencePhase === 'decode'}
progressLabel="Rate / 50 tok/s"
/>
</div>
<div className="device-card">
<span className="device-icon" aria-hidden="true"><i /><i /><i /></span>
<div>
<small>Active adapter</small>
<strong>{telemetry.device}</strong>
</div>
<span>{telemetry.gpuLayers}<small>layers</small></span>
</div>
<Meter label="Context" value={contextRatio} detail={`${telemetry.contextUsed.toLocaleString()} / ${telemetry.contextLimit.toLocaleString()}`} />
<div className="memory-grid">
<div><small>Model allocation</small><strong>{formatBytes(telemetry.modelMemoryBytes)}</strong></div>
<div><small>KV allocation</small><strong>{formatBytes(telemetry.kvMemoryBytes)}</strong></div>
</div>
</section>
<section className="inspector-section tool-log-section" aria-labelledby="tool-log-title">
<div className="section-heading compact">
<h2 id="tool-log-title">Tool log</h2>
</div>
<ol className="event-log">
{toolEvents.length === 0 && <li className="event-empty">No tool calls in this session.</li>}
{toolEvents.map((event) => (
<li key={event.id}>
<span className={`event-pip event-${event.state}`} />
<time>{event.timestamp}</time>
<div><strong>{event.label}</strong><small>{event.detail}</small></div>
</li>
))}
</ol>
</section>
<section className="inspector-section artifact-index-section" aria-labelledby="artifact-index-title">
<div className="section-heading compact">
<h2 id="artifact-index-title">Artifacts</h2>
<span className="artifact-count" aria-label={`${artifacts.length} artifacts`}>{artifacts.length}</span>
</div>
{artifacts.length === 0 ? (
<p className="artifact-empty">Generated HTML artifacts will be indexed here and rendered in the conversation.</p>
) : (
<ol className="artifact-index">
{artifacts.map((artifact, index) => (
<li key={artifact.id}>
<button type="button" onClick={() => onSelectArtifact(artifact.id)}>
<span>{String(index + 1).padStart(2, '0')}</span>
<strong>{artifact.title}</strong>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7 4 6 6-6 6" /></svg>
</button>
</li>
))}
</ol>
)}
</section>
</aside>
);
}
function TelemetryMetric({
label,
value,
detail,
ratio,
active,
progressLabel,
}: {
label: string;
value: string;
detail: string;
ratio: number;
active: boolean;
progressLabel: string;
}) {
const percentage = Math.round(Math.min(1, Math.max(0, ratio)) * 100);
return (
<div className={`telemetry-metric${active ? ' is-active' : ''}`}>
<div><span>{label}</span><strong>{value}</strong></div>
<small>{detail}</small>
<div className="telemetry-meter" role="meter" aria-label={`${label}: ${progressLabel}`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={percentage}>
<span style={{ width: `${percentage}%` }} />
</div>
</div>
);
}
function Meter({ label, value, detail }: { label: string; value: number; detail: string }) {
const percentage = Math.round(Math.min(1, Math.max(0, value)) * 100);
return (
<div className="meter">
<div><span>{label}</span><small>{detail}</small></div>
<div className="meter-track" role="meter" aria-label={label} aria-valuemin={0} aria-valuemax={100} aria-valuenow={percentage}>
<span style={{ width: `${percentage}%` }} />
</div>
</div>
);
}