Initial commit: LLM Automation Docs & Remediation Engine v2.0
Features: - Automated datacenter documentation generation - MCP integration for device connectivity - Auto-remediation engine with safety checks - Multi-factor reliability scoring (0-100%) - Human feedback learning loop - Pattern recognition and continuous improvement - Agentic chat support with AI - API for ticket resolution - Frontend React with Material-UI - CI/CD pipelines (GitLab + Gitea) - Docker & Kubernetes deployment - Complete documentation and guides v2.0 Highlights: - Auto-remediation with write operations (disabled by default) - Reliability calculator with 4-factor scoring - Human feedback system for continuous learning - Pattern-based progressive automation - Approval workflow for critical actions - Full audit trail and rollback capability
This commit is contained in:
668
frontend/src/App_Enhanced.jsx
Normal file
668
frontend/src/App_Enhanced.jsx
Normal file
@@ -0,0 +1,668 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
AppBar, Toolbar, Typography, Container, Box, Paper,
|
||||
TextField, Button, List, ListItem, ListItemText,
|
||||
CircularProgress, Chip, Grid, Card, CardContent,
|
||||
Tabs, Tab, Divider, IconButton, Switch, FormControlLabel,
|
||||
Alert, AlertTitle, Dialog, DialogTitle, DialogContent,
|
||||
DialogActions, Rating, LinearProgress, Tooltip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Send as SendIcon,
|
||||
ThumbUp, ThumbDown, Warning as WarningIcon,
|
||||
CheckCircle, Info, Shield, Speed, TrendingUp
|
||||
} from '@mui/icons-material';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<AppBar position="static" sx={{ bgcolor: '#1976d2' }}>
|
||||
<Toolbar>
|
||||
<Shield sx={{ mr: 2 }} />
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
Datacenter AI System - Auto-Remediation Enabled
|
||||
</Typography>
|
||||
<Chip label="AI Powered" color="secondary" size="small" sx={{ mr: 1 }} />
|
||||
<Chip label="v2.0" color="success" size="small" />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={activeTab} onChange={(e, v) => setActiveTab(v)}>
|
||||
<Tab label="Submit Ticket" />
|
||||
<Tab label="Ticket Status" />
|
||||
<Tab label="Feedback Center" />
|
||||
<Tab label="Analytics" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<Container maxWidth="xl" sx={{ mt: 4, mb: 4 }}>
|
||||
{activeTab === 0 && <TicketSubmitInterface />}
|
||||
{activeTab === 1 && <TicketStatusInterface />}
|
||||
{activeTab === 2 && <FeedbackCenter />}
|
||||
{activeTab === 3 && <AnalyticsDashboard />}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Ticket Submit Interface with Auto-Remediation Toggle
|
||||
function TicketSubmitInterface() {
|
||||
const [ticketData, setTicketData] = useState({
|
||||
ticket_id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
priority: 'medium',
|
||||
category: '',
|
||||
enable_auto_remediation: false // DEFAULT: DISABLED
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState(null);
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
|
||||
const submitTicket = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/api/v1/tickets`, ticketData);
|
||||
setResult(response.data);
|
||||
|
||||
// Poll for updates
|
||||
const ticketId = response.data.ticket_id;
|
||||
const pollInterval = setInterval(async () => {
|
||||
const statusResponse = await axios.get(`${API_URL}/api/v1/tickets/${ticketId}`);
|
||||
setResult(statusResponse.data);
|
||||
|
||||
if (statusResponse.data.status === 'resolved' ||
|
||||
statusResponse.data.status === 'failed') {
|
||||
clearInterval(pollInterval);
|
||||
setLoading(false);
|
||||
}
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Submit Ticket for AI Resolution
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Ticket ID"
|
||||
value={ticketData.ticket_id}
|
||||
onChange={(e) => setTicketData({...ticketData, ticket_id: e.target.value})}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Title"
|
||||
value={ticketData.title}
|
||||
onChange={(e) => setTicketData({...ticketData, title: e.target.value})}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
label="Problem Description"
|
||||
value={ticketData.description}
|
||||
onChange={(e) => setTicketData({...ticketData, description: e.target.value})}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
label="Category"
|
||||
value={ticketData.category}
|
||||
onChange={(e) => setTicketData({...ticketData, category: e.target.value})}
|
||||
margin="normal"
|
||||
SelectProps={{ native: true }}
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
<option value="network">Network</option>
|
||||
<option value="server">Server</option>
|
||||
<option value="storage">Storage</option>
|
||||
<option value="security">Security</option>
|
||||
<option value="backup">Backup</option>
|
||||
</TextField>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Box sx={{ bgcolor: '#fff3e0', p: 2, borderRadius: 1, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<WarningIcon color="warning" sx={{ mr: 1 }} />
|
||||
<Typography variant="subtitle2" color="warning.dark">
|
||||
Auto-Remediation Control
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={ticketData.enable_auto_remediation}
|
||||
onChange={(e) => {
|
||||
setTicketData({
|
||||
...ticketData,
|
||||
enable_auto_remediation: e.target.checked
|
||||
});
|
||||
if (e.target.checked) setShowWarning(true);
|
||||
}}
|
||||
color="warning"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2">
|
||||
Enable Auto-Remediation (Write Operations)
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
|
||||
<Typography variant="caption" display="block" color="text.secondary">
|
||||
When enabled, AI can automatically execute fixes on your infrastructure.
|
||||
Default: DISABLED for safety. Only enable if you trust AI decisions.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={submitTicket}
|
||||
disabled={loading}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : 'Submit Ticket'}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
{result && <TicketResultDisplay result={result} />}
|
||||
</Grid>
|
||||
|
||||
{/* Warning Dialog */}
|
||||
<Dialog open={showWarning} onClose={() => setShowWarning(false)}>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<WarningIcon color="warning" sx={{ mr: 1 }} />
|
||||
Auto-Remediation Warning
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
You are enabling auto-remediation. This means:
|
||||
</Alert>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="✓ AI can execute WRITE operations on your infrastructure"
|
||||
secondary="Includes: restarting services, modifying configs, scaling resources"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="✓ Actions are based on reliability scores and learned patterns"
|
||||
secondary="Only high-confidence actions with ≥85% reliability are auto-executed"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="✓ All actions are logged and can be rolled back"
|
||||
secondary="Safety checks and pre/post validation included"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="⚠️ Critical actions require human approval"
|
||||
secondary="Destructive operations always need manual confirmation"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowWarning(false)}>
|
||||
I Understand
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced Ticket Result Display
|
||||
function TicketResultDisplay({ result }) {
|
||||
const getConfidenceColor = (level) => {
|
||||
const colors = {
|
||||
'very_high': 'success',
|
||||
'high': 'info',
|
||||
'medium': 'warning',
|
||||
'low': 'error'
|
||||
};
|
||||
return colors[level] || 'default';
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Resolution & Analysis
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Chip
|
||||
label={result.status}
|
||||
color={result.status === 'resolved' ? 'success' : 'warning'}
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
{result.auto_remediation_enabled && (
|
||||
<Chip
|
||||
icon={<Shield />}
|
||||
label="Auto-Remediation Enabled"
|
||||
color="warning"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Reliability Scores */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
AI Confidence & Reliability
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography variant="body2">AI Confidence</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{(result.confidence_score * 100).toFixed(0)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={result.confidence_score * 100}
|
||||
color="primary"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{result.reliability_score && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography variant="body2">Reliability Score</Typography>
|
||||
<Chip
|
||||
label={result.confidence_level || 'calculating'}
|
||||
color={getConfidenceColor(result.confidence_level)}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={result.reliability_score}
|
||||
color={result.reliability_score >= 85 ? 'success' : 'warning'}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Based on: AI confidence, historical success, feedback, patterns
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Resolution */}
|
||||
<Typography variant="subtitle2" gutterBottom>Resolution:</Typography>
|
||||
<Typography variant="body1" paragraph>{result.resolution}</Typography>
|
||||
|
||||
{/* Suggested Actions */}
|
||||
{result.suggested_actions?.length > 0 && (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>Suggested Actions:</Typography>
|
||||
<List dense>
|
||||
{result.suggested_actions.map((action, idx) => (
|
||||
<ListItem key={idx}>
|
||||
<ListItemText primary={`${idx + 1}. ${action.action || action}`} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Auto-Remediation Status */}
|
||||
{result.auto_remediation_enabled && (
|
||||
<Alert
|
||||
severity={result.auto_remediation_executed ? 'success' : 'info'}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
<AlertTitle>
|
||||
{result.auto_remediation_executed ? 'Auto-Remediation Executed' : 'Auto-Remediation Status'}
|
||||
</AlertTitle>
|
||||
{result.remediation_decision && (
|
||||
<Typography variant="body2">
|
||||
{result.remediation_decision.allowed
|
||||
? `✓ Actions approved for execution (${result.remediation_decision.action_type})`
|
||||
: `✗ Actions require manual intervention: ${result.remediation_decision.reasoning.join(', ')}`
|
||||
}
|
||||
</Typography>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="caption">
|
||||
Processing Time: {result.processing_time?.toFixed(2)}s
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
// Ticket Status Interface
|
||||
function TicketStatusInterface() {
|
||||
const [ticketId, setTicketId] = useState('');
|
||||
const [ticket, setTicket] = useState(null);
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchTicket = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/v1/tickets/${ticketId}`);
|
||||
setTicket(response.data);
|
||||
|
||||
// Fetch logs if auto-remediation was executed
|
||||
if (response.data.auto_remediation_executed) {
|
||||
const logsResponse = await axios.get(`${API_URL}/api/v1/tickets/${ticketId}/remediation-logs`);
|
||||
setLogs(logsResponse.data.logs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={ticketId}
|
||||
onChange={(e) => setTicketId(e.target.value)}
|
||||
placeholder="Enter Ticket ID"
|
||||
/>
|
||||
<Button variant="contained" onClick={fetchTicket} disabled={loading}>
|
||||
Fetch
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{ticket && (
|
||||
<>
|
||||
<Grid item xs={12} md={8}>
|
||||
<TicketResultDisplay result={ticket} />
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<FeedbackForm ticketId={ticketId} />
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{logs.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<RemediationLogsDisplay logs={logs} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
// Feedback Form
|
||||
function FeedbackForm({ ticketId }) {
|
||||
const [feedback, setFeedback] = useState({
|
||||
feedback_type: 'positive',
|
||||
rating: 5,
|
||||
was_helpful: true,
|
||||
resolution_accurate: true,
|
||||
actions_worked: true,
|
||||
comment: ''
|
||||
});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const submitFeedback = async () => {
|
||||
try {
|
||||
await axios.post(`${API_URL}/api/v1/feedback`, {
|
||||
ticket_id: ticketId,
|
||||
...feedback
|
||||
});
|
||||
setSubmitted(true);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<ThumbUp sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Provide Feedback
|
||||
</Typography>
|
||||
|
||||
{submitted ? (
|
||||
<Alert severity="success">
|
||||
<AlertTitle>Thank You!</AlertTitle>
|
||||
Your feedback helps improve the AI system.
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Was this resolution helpful?
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ my: 2 }}>
|
||||
<Rating
|
||||
value={feedback.rating}
|
||||
onChange={(e, value) => setFeedback({...feedback, rating: value})}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={feedback.resolution_accurate}
|
||||
onChange={(e) => setFeedback({
|
||||
...feedback,
|
||||
resolution_accurate: e.target.checked,
|
||||
feedback_type: e.target.checked ? 'positive' : 'negative'
|
||||
})}
|
||||
/>
|
||||
}
|
||||
label="Resolution was accurate"
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={feedback.actions_worked}
|
||||
onChange={(e) => setFeedback({...feedback, actions_worked: e.target.checked})}
|
||||
/>
|
||||
}
|
||||
label="Suggested actions worked"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label="Comments (optional)"
|
||||
value={feedback.comment}
|
||||
onChange={(e) => setFeedback({...feedback, comment: e.target.value})}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={submitFeedback}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Submit Feedback
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
// Remediation Logs Display
|
||||
function RemediationLogsDisplay({ logs }) {
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Auto-Remediation Execution Logs
|
||||
</Typography>
|
||||
|
||||
<List>
|
||||
{logs.map((log, idx) => (
|
||||
<ListItem key={idx} divider>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{log.success ?
|
||||
<CheckCircle color="success" fontSize="small" /> :
|
||||
<WarningIcon color="error" fontSize="small" />
|
||||
}
|
||||
<Typography variant="body2">{log.action}</Typography>
|
||||
<Chip label={log.type} size="small" />
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
<Typography variant="caption" display="block">
|
||||
Target: {log.target_system} / {log.target_resource}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block">
|
||||
Executed: {new Date(log.executed_at).toLocaleString()}
|
||||
</Typography>
|
||||
{log.error && (
|
||||
<Typography variant="caption" color="error" display="block">
|
||||
Error: {log.error}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
// Feedback Center
|
||||
function FeedbackCenter() {
|
||||
// Implementation for viewing all feedback and metrics
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6">Feedback Center</Typography>
|
||||
<Typography variant="body2">
|
||||
View all feedback, improve AI accuracy, and track pattern learning.
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
// Analytics Dashboard
|
||||
function AnalyticsDashboard() {
|
||||
const [stats, setStats] = useState(null);
|
||||
const [autoRemStats, setAutoRemStats] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const [reliability, autoRem] = await Promise.all([
|
||||
axios.get(`${API_URL}/api/v1/stats/reliability`),
|
||||
axios.get(`${API_URL}/api/v1/stats/auto-remediation`)
|
||||
]);
|
||||
setStats(reliability.data);
|
||||
setAutoRemStats(autoRem.data);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={3}>
|
||||
<StatCard
|
||||
title="Avg Reliability"
|
||||
value={`${stats?.avg_reliability || 0}%`}
|
||||
icon={<Speed />}
|
||||
color="primary"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<StatCard
|
||||
title="Avg Confidence"
|
||||
value={`${stats?.avg_confidence || 0}%`}
|
||||
icon={<TrendingUp />}
|
||||
color="success"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<StatCard
|
||||
title="Resolution Rate"
|
||||
value={`${stats?.resolution_rate || 0}%`}
|
||||
icon={<CheckCircle />}
|
||||
color="info"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<StatCard
|
||||
title="Auto-Rem Success"
|
||||
value={`${autoRemStats?.success_rate || 0}%`}
|
||||
icon={<Shield />}
|
||||
color="warning"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ title, value, icon, color }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography color="text.secondary" gutterBottom variant="body2">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{value}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ color: `${color}.main` }}>
|
||||
{icon}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user