/** * DiagnosticPanel — AI Diagnostics display component (Story 18.10) * * Shows severity badge, AI label, summary, collapsible root causes with * evidence, recommendations with priority, and LLM metadata footer. */ import React, { useState } from 'react'; import type { DiagnosticReport, RootCause, Recommendation } from '../api/diagnose'; // ─── Severity styling ─────────────────────────────────────────── const SEVERITY_STYLES: Record< DiagnosticReport['severity'], { bg: string; text: string; border: string; icon: string } > = { critical: { bg: 'bg-red-50', text: 'text-red-800', border: 'border-red-300', icon: '🔴' }, warning: { bg: 'bg-orange-50', text: 'text-orange-800', border: 'border-orange-300', icon: '🟠' }, info: { bg: 'bg-blue-50', text: 'text-blue-800', border: 'border-blue-300', icon: '🔵' }, healthy: { bg: 'bg-green-50', text: 'text-green-800', border: 'border-green-300', icon: '🟢' }, }; const PRIORITY_STYLES: Record = { high: { badge: 'bg-red-100 text-red-700', label: 'HIGH' }, medium: { badge: 'bg-yellow-100 text-yellow-700', label: 'MED' }, low: { badge: 'bg-gray-100 text-gray-600', label: 'LOW' }, }; // ─── Sub-components ───────────────────────────────────────────── function SeverityBadge({ severity }: { severity: DiagnosticReport['severity'] }) { const s = SEVERITY_STYLES[severity]; return ( {s.icon} {severity.charAt(0).toUpperCase() + severity.slice(1)} ); } function ConfidenceBar({ confidence }: { confidence: number }) { const pct = Math.round(confidence * 100); const color = pct >= 75 ? 'bg-green-500' : pct >= 50 ? 'bg-yellow-500' : 'bg-red-500'; return (
{pct}%
); } function RootCauseItem({ cause, index }: { cause: RootCause; index: number }) { const [expanded, setExpanded] = useState(false); return (
{expanded && cause.evidence.length > 0 && (
{cause.evidence.map((ev, i) => (
{ev.summary}
))}
)}
); } function RecommendationItem({ rec }: { rec: Recommendation }) { const p = PRIORITY_STYLES[rec.priority]; return (
{p.label}

{rec.action}

{rec.rationale}

); } // ─── Loading skeleton ─────────────────────────────────────────── function DiagnosticSkeleton() { return (
🧠 Running AI diagnostics… this may take 5–15 seconds
); } // ─── Main component ───────────────────────────────────────────── export interface DiagnosticPanelProps { report: DiagnosticReport | null; loading: boolean; error?: string | null; onRefresh?: () => void; } export function DiagnosticPanel({ report, loading, error, onRefresh, }: DiagnosticPanelProps): React.ReactElement { if (loading) return ; if (error) { return (

Failed to run diagnostics

{error}

{onRefresh && ( )}
); } if (!report) return <>; const isFallback = report.source === 'fallback'; return (
{/* Header: severity + AI label + refresh */}
🤖 AI-suggested {isFallback && ( ⚠️ AI diagnostics unavailable — heuristic analysis )}
{onRefresh && ( )}
{/* Health score */} {report.healthScore !== undefined && (
Health Score: {Math.round(report.healthScore)}/100
)} {/* Summary */}

Summary

{report.summary}

{/* Root Causes */} {report.rootCauses.length > 0 && (

Root Causes ({report.rootCauses.length})

{report.rootCauses.map((cause, i) => ( ))}
)} {/* Recommendations */} {report.recommendations.length > 0 && (

Recommendations ({report.recommendations.length})

{report.recommendations.map((rec, i) => ( ))}
)} {/* LLM Metadata footer */} {!isFallback && (
Provider: {report.llmMeta.provider} Model: {report.llmMeta.model} Tokens: {report.llmMeta.inputTokens + report.llmMeta.outputTokens} Cost: ${report.llmMeta.estimatedCostUsd.toFixed(4)} Latency: {(report.llmMeta.latencyMs / 1000).toFixed(1)}s
)} {/* Analysis context */}
Based on {report.analysisContext.sessionCount} sessions,{' '} {report.analysisContext.dataPoints} data points {report.analysisContext.windowDays && ` over ${report.analysisContext.windowDays} days`}
); }