2026-04-13-4
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Activity,
|
||||
Map as MapIcon,
|
||||
Settings,
|
||||
ShieldAlert,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
Search,
|
||||
LayoutDashboard,
|
||||
ExternalLink,
|
||||
ChevronRight
|
||||
} from 'lucide-react';
|
||||
|
||||
const IntelligenceDashboard: React.FC = () => {
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [candidates, setCandidates] = useState<any[]>([]);
|
||||
const [closures, setClosures] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'summary' | 'candidates' | 'closures'>('summary');
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const API_BASE = (import.meta as any).env.VITE_API_URL || '/api';
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [summaryRes, candRes, closeRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/map-refinement/summary`),
|
||||
fetch(`${API_BASE}/map-refinement/candidates?status=pending`),
|
||||
fetch(`${API_BASE}/map-refinement/closures`)
|
||||
]);
|
||||
|
||||
const summary = await summaryRes.json();
|
||||
const cand = await candRes.json();
|
||||
const close = await closeRes.json();
|
||||
|
||||
setStats(summary);
|
||||
setCandidates(cand || []);
|
||||
setClosures(close || []);
|
||||
} catch (error) {
|
||||
console.error("Dashboard fetch failed", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const runAnalysis = async () => {
|
||||
setMessage('Brain starting deep analysis... 🧠');
|
||||
try {
|
||||
await fetch(`${API_BASE}/telemetry/process-intelligence?days=2`, { method: 'POST' });
|
||||
setMessage('Analysis initiated successfully. Check Telegram for report.');
|
||||
} catch (error) {
|
||||
setMessage('Analysis failed to start.');
|
||||
}
|
||||
setTimeout(() => setMessage(''), 5000);
|
||||
};
|
||||
|
||||
const handleCandidate = async (id: string, action: 'approve' | 'reject') => {
|
||||
try {
|
||||
await fetch(`${API_BASE}/map-refinement/candidates/${id}/${action}`, { method: 'PATCH' });
|
||||
setMessage(`Road ${action}d successfully. 🛣️`);
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
setMessage(`Failed to ${action} road.`);
|
||||
}
|
||||
setTimeout(() => setMessage(''), 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="intel-dashboard" style={{
|
||||
color: '#f8fafc',
|
||||
padding: '2rem',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
animation: 'fadeIn 0.5s ease'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2.5rem' }}>
|
||||
<div>
|
||||
<h1 style={{ fontSize: '1.8rem', fontWeight: 700, margin: 0, display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{ background: 'linear-gradient(135deg, #6366f1, #3b82f6)', padding: '10px', borderRadius: '12px' }}>
|
||||
<LayoutDashboard size={24} color="white" />
|
||||
</div>
|
||||
Map AI Intelligence
|
||||
</h1>
|
||||
<p style={{ color: '#94a3b8', margin: '4px 0 0 0', fontSize: '0.9rem' }}>Intaleq SaaS Monitoring & Enrichment Platform</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button
|
||||
onClick={runAnalysis}
|
||||
className="btn-intel"
|
||||
style={{
|
||||
background: '#6366f1',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={18} /> Run Intelligence
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Stats Quick View */}
|
||||
<section style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '1.5rem', marginBottom: '2rem' }}>
|
||||
{[
|
||||
{ label: 'Telemetry Points', value: stats?.telemetry?.total || 0, icon: <Activity size={20} color="#818cf8" />, color: '#818cf8' },
|
||||
{ label: 'Analyzed Segments', value: stats?.roads?.analyzed || 0, icon: <MapIcon size={20} color="#60a5fa" />, color: '#60a5fa' },
|
||||
{ label: 'Road Candidates', value: candidates.length, icon: <Settings size={20} color="#fbbf24" />, color: '#fbbf24' },
|
||||
{ label: 'Active Closures', value: closures.length, icon: <ShieldAlert size={20} color="#f87171" />, color: '#f87171' },
|
||||
].map((stat, i) => (
|
||||
<div key={i} style={{
|
||||
background: 'rgba(30, 41, 59, 0.7)',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '16px',
|
||||
padding: '1.5rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<div style={{ background: `${stat.color}15`, padding: '12px', borderRadius: '12px' }}>{stat.icon}</div>
|
||||
<div>
|
||||
<span style={{ fontSize: '0.8rem', color: '#94a3b8', display: 'block' }}>{stat.label}</span>
|
||||
<span style={{ fontSize: '1.4rem', fontWeight: 700 }}>{stat.value.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div style={{
|
||||
background: 'rgba(30, 41, 59, 0.7)',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '24px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Navigation Tabs */}
|
||||
<nav style={{ display: 'flex', borderBottom: '1px solid #334155', background: 'rgba(15, 23, 42, 0.3)' }}>
|
||||
{[
|
||||
{ id: 'summary', label: 'Analysis Summary' },
|
||||
{ id: 'candidates', label: `Candidates (${candidates.length})` },
|
||||
{ id: 'closures', label: `Closures (${closures.length})` }
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as any)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '1.2rem',
|
||||
border: 'none',
|
||||
background: activeTab === tab.id ? 'rgba(99, 102, 241, 0.1)' : 'transparent',
|
||||
color: activeTab === tab.id ? '#818cf8' : '#94a3b8',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
borderBottom: activeTab === tab.id ? '2px solid #818cf8' : '2px solid transparent',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div style={{ padding: '2rem', minHeight: '400px' }}>
|
||||
{activeTab === 'candidates' && (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
|
||||
<h3 style={{ margin: 0 }}>Detected Road Candidates / طرق مرشحة</h3>
|
||||
<div style={{ display: 'flex', gap: '8px', background: '#1e293b', padding: '6px 12px', borderRadius: '10px' }}>
|
||||
<Search size={16} color="#94a3b8" />
|
||||
<input type="text" placeholder="Filter candidates..." style={{ background: 'none', border: 'none', color: 'white', outline: 'none', fontSize: '0.85rem' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{candidates.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '4rem', color: '#64748b' }}>
|
||||
<Activity size={48} style={{ opacity: 0.1, marginBottom: '1rem' }} />
|
||||
<p>No pending road candidates found. Everything is synced. ✅</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||
{candidates.map((c) => (
|
||||
<div key={c.id} style={{
|
||||
background: '#1e293b',
|
||||
borderRadius: '16px',
|
||||
padding: '1.2rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
border: '1px solid #334155'
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
|
||||
<div style={{
|
||||
width: '4px',
|
||||
height: '40px',
|
||||
background: c.confidence > 0.8 ? '#22c55e' : '#eab308',
|
||||
borderRadius: '4px'
|
||||
}} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: '1rem', display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
New Segment {c.id.slice(-4)}
|
||||
<span style={{
|
||||
fontSize: '0.7rem',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '20px',
|
||||
color: '#94a3b8'
|
||||
}}>
|
||||
{Math.round(c.lengthMeters)}m
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.8rem', marginTop: '4px' }}>
|
||||
{c.uniqueDriverCount} drivers • {c.totalPoints} points • Confidence: {Math.round(c.confidence * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button onClick={() => handleCandidate(c.id, 'approve')} style={{ background: 'rgba(34, 197, 94, 0.1)', color: '#4ade80', border: '1px solid #22c55e33', padding: '8px 16px', borderRadius: '8px', cursor: 'pointer', fontWeight: 600 }}>Approve</button>
|
||||
<button onClick={() => handleCandidate(c.id, 'reject')} style={{ background: 'rgba(239, 68, 68, 0.1)', color: '#f87171', border: '1px solid #ef444433', padding: '8px 16px', borderRadius: '8px', cursor: 'pointer', fontWeight: 600 }}>Ignore</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'summary' && stats && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1.2fr', gap: '2rem' }}>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '1.5rem' }}>Intelligence Quality Report</h3>
|
||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||
{/* Quality Metrics */}
|
||||
{[
|
||||
{ label: 'Data Density', value: 'High Accuracy', desc: 'Clusters are well-defined in Damascus & Amman', color: '#22c55e' },
|
||||
{ label: 'Map Freshness', value: 'Last 24h', desc: 'Telemetry analysis window is fully processed', color: '#6366f1' },
|
||||
{ label: 'Overpass Sync', value: 'Available', desc: 'Overture enrichment ready to pull labels', color: '#3b82f6' }
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{ background: '#1e293b', padding: '1.2rem', borderRadius: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{item.label}</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.8rem' }}>{item.desc}</div>
|
||||
</div>
|
||||
<div style={{ color: item.color, fontWeight: 700, fontSize: '0.9rem' }}>{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ background: '#1e293b', borderRadius: '24px', padding: '1.5rem', border: '1px solid #334155' }}>
|
||||
<h4 style={{ margin: '0 0 1rem 0' }}>Action Log</h4>
|
||||
<div style={{ fontSize: '0.85rem', color: '#94a3b8', display: 'grid', gap: '12px' }}>
|
||||
{stats.history?.map((entry: any, i: number) => (
|
||||
<div key={i} style={{ display: 'flex', gap: '10px' }}>
|
||||
<ChevronRight size={14} />
|
||||
{entry}
|
||||
</div>
|
||||
)) || [<div key="0">System initialized. Waiting for analysis...</div>]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'closures' && (
|
||||
<div style={{ textAlign: 'center', padding: '4rem', color: '#64748b' }}>
|
||||
<XCircle size={48} style={{ opacity: 0.1, marginBottom: '1rem' }} color="#f87171" />
|
||||
<p>No active road closures detected in current telemetry. 🚧</p>
|
||||
<button className="btn-intel" style={{ background: 'transparent', border: '1px solid #334155', color: '#94a3b8', padding: '8px 16px', borderRadius: '8px', cursor: 'pointer', marginTop: '1rem' }}>Trigger Manual Closure Scan</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Persistence Message */}
|
||||
{message && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: '2rem',
|
||||
right: '2rem',
|
||||
background: '#0f172a',
|
||||
border: '1px solid #6366f1',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.5)',
|
||||
animation: 'slideIn 0.3s ease-out',
|
||||
zIndex: 1000,
|
||||
color: 'white',
|
||||
fontWeight: 600
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
`}} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntelligenceDashboard;
|
||||
Reference in New Issue
Block a user