Spaces:
Running
Running
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| AnalysisView β Live analysis split-panel dashboard | |
| Left: Agent status cards | Right: Live findings feed | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| import { useEffect, useRef, useMemo } from 'react'; | |
| import { useScan, SCAN_STATUS, VIEWS } from '../context/ScanContext'; | |
| import AgentCard from './AgentCard'; | |
| import FindingCard from './FindingCard'; | |
| import AMDMetricsCard from './AMDMetricsCard'; | |
| import './AnalysisView.css'; | |
| function formatTime(ms) { | |
| const seconds = Math.floor(ms / 1000); | |
| const minutes = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| return `${minutes}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| export default function AnalysisView() { | |
| const { | |
| scanStatus, agents, findings, fixes, elapsedTime, | |
| summary, setView, resetScan, amdMetrics, error | |
| } = useScan(); | |
| const feedRef = useRef(null); | |
| // Auto-scroll findings feed | |
| useEffect(() => { | |
| if (feedRef.current) { | |
| feedRef.current.scrollTop = feedRef.current.scrollHeight; | |
| } | |
| }, [findings, fixes]); | |
| // Build fix map for quick lookup | |
| const fixMap = useMemo(() => { | |
| const map = {}; | |
| fixes.forEach(fix => { | |
| map[fix.findingId] = fix; | |
| }); | |
| return map; | |
| }, [fixes]); | |
| // Severity counts | |
| const severityCounts = useMemo(() => { | |
| const counts = { critical: 0, high: 0, medium: 0, low: 0 }; | |
| findings.forEach(f => { | |
| if (counts[f.severity] !== undefined) counts[f.severity]++; | |
| }); | |
| return counts; | |
| }, [findings]); | |
| const isComplete = scanStatus === SCAN_STATUS.COMPLETE; | |
| return ( | |
| <div className="analysis-view"> | |
| {/* Header Bar */} | |
| <header className="header-bar"> | |
| <div className="header-logo" onClick={resetScan} style={{ cursor: 'pointer' }}> | |
| <span className="shield-icon">π‘οΈ</span> | |
| <span className="logo-text">CodeSentry</span> | |
| </div> | |
| <div className="header-center"> | |
| <div className="scan-status-indicator"> | |
| <span className={`status-dot ${isComplete ? 'complete' : 'scanning'}`} /> | |
| <span className="mono" style={{ fontSize: '0.8rem' }}> | |
| {isComplete ? 'SCAN COMPLETE' : 'SCANNING...'} | |
| </span> | |
| </div> | |
| <span className="elapsed-time mono">{formatTime(elapsedTime)}</span> | |
| </div> | |
| <div className="header-actions"> | |
| {isComplete && ( | |
| <button | |
| id="view-report-btn" | |
| className="btn btn-primary btn-sm" | |
| onClick={() => setView(VIEWS.REPORT)} | |
| > | |
| π View Report | |
| </button> | |
| )} | |
| <button | |
| id="new-scan-btn" | |
| className="btn btn-ghost btn-sm" | |
| onClick={resetScan} | |
| > | |
| β» New Scan | |
| </button> | |
| </div> | |
| </header> | |
| {/* Main Split Panel */} | |
| <div className="split-panel"> | |
| {/* Left Panel β Agent Status */} | |
| <aside className="agents-panel"> | |
| <div className="panel-header"> | |
| <h3>AI Agents</h3> | |
| <span className="tag">{Object.values(agents).filter(a => a.status === 'complete').length}/3</span> | |
| </div> | |
| <div className="agents-list"> | |
| <AgentCard agentId="security" agentState={agents.security} /> | |
| <AgentCard agentId="performance" agentState={agents.performance} /> | |
| <AgentCard agentId="fix" agentState={agents.fix} /> | |
| </div> | |
| {/* Live Stats Summary */} | |
| <div className="live-stats glass-card-static"> | |
| <div className="live-stats-header"> | |
| <span>π</span> | |
| <span>Live Statistics</span> | |
| </div> | |
| <div className="stats-grid"> | |
| <div className="live-stat"> | |
| <span className="live-stat-value text-critical">{severityCounts.critical}</span> | |
| <span className="live-stat-label">Critical</span> | |
| </div> | |
| <div className="live-stat"> | |
| <span className="live-stat-value text-high">{severityCounts.high}</span> | |
| <span className="live-stat-label">High</span> | |
| </div> | |
| <div className="live-stat"> | |
| <span className="live-stat-value text-medium">{severityCounts.medium}</span> | |
| <span className="live-stat-label">Medium</span> | |
| </div> | |
| <div className="live-stat"> | |
| <span className="live-stat-value text-low">{severityCounts.low}</span> | |
| <span className="live-stat-label">Low</span> | |
| </div> | |
| </div> | |
| <div className="total-findings"> | |
| <span>Total findings:</span> | |
| <strong>{findings.length}</strong> | |
| </div> | |
| <div className="total-findings"> | |
| <span>Fixes generated:</span> | |
| <strong className="text-low">{fixes.length}</strong> | |
| </div> | |
| </div> | |
| {/* AMD MI300X Live Metrics */} | |
| <AMDMetricsCard | |
| amdMetrics={amdMetrics} | |
| isComplete={isComplete} | |
| scanDuration={isComplete ? Math.round(elapsedTime / 1000) : null} | |
| /> | |
| </aside> | |
| {/* Right Panel β Live Findings Feed */} | |
| <main className="findings-panel" ref={feedRef}> | |
| <div className="panel-header"> | |
| <h3>Live Findings</h3> | |
| <span className="findings-count mono">{findings.length} findings</span> | |
| </div> | |
| {findings.length === 0 && scanStatus !== SCAN_STATUS.ERROR && ( | |
| <div className="empty-state"> | |
| <div className="icon">π</div> | |
| <p>Waiting for agents to report findings...</p> | |
| <div className="scanning-indicator"> | |
| <div className="progress-bar-track" style={{ width: '200px' }}> | |
| <div className="progress-bar-fill cyan" style={{ width: '60%' }} /> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {scanStatus === SCAN_STATUS.ERROR && ( | |
| <div className="error-banner glass-card" style={{ borderColor: 'rgba(2ef4, 63, 94, 0.5)', background: 'rgba(225, 29, 72, 0.1)' }}> | |
| <div className="completion-icon">β</div> | |
| <div className="completion-content"> | |
| <h4 style={{ color: '#fb7185' }}>Analysis Failed</h4> | |
| <p>{error || "An unknown error occurred during the analysis stream."}</p> | |
| </div> | |
| <button className="btn btn-ghost" onClick={resetScan}> | |
| Try Again | |
| </button> | |
| </div> | |
| )} | |
| <div className="findings-feed"> | |
| {findings.map((finding, index) => ( | |
| <FindingCard | |
| key={finding.id || index} | |
| finding={finding} | |
| index={index} | |
| fix={fixMap[finding.id]} | |
| /> | |
| ))} | |
| {/* Fix events shown as special cards */} | |
| {fixes.filter(fix => !findings.find(f => f.id === fix.findingId)).map((fix, index) => ( | |
| <div key={`fix-${index}`} className="fix-event-card glass-card-static animate-slide-in"> | |
| <div className="fix-event-header"> | |
| <span>π§</span> | |
| <span className="fix-event-title">{fix.title}</span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Completion Banner */} | |
| {isComplete && ( | |
| <div className="completion-banner glass-card animate-fade-in-up"> | |
| <div className="completion-icon">β </div> | |
| <div className="completion-content"> | |
| <h4>Analysis Complete</h4> | |
| <p> | |
| Found {summary?.totalFindings || findings.length} issues across {summary?.filesAnalyzed || '24'} files | |
| in {formatTime(elapsedTime)}. {fixes.length} automated fixes generated. | |
| </p> | |
| </div> | |
| <button | |
| className="btn btn-primary" | |
| onClick={() => setView(VIEWS.REPORT)} | |
| > | |
| π View Full Report | |
| </button> | |
| </div> | |
| )} | |
| </main> | |
| </div> | |
| </div> | |
| ); | |
| } | |