Spaces:
Running
Running
| 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> | |
| ); | |
| } | |