auth / client /src /tabs /IntelTab.jsx
Piyush1225's picture
UPDATE: UI and client assets
808332c
import { useState, useEffect, useRef, useCallback } from 'react';
import { req, getToken, saveToken, ENDPOINTS } from '../api';
import { Card, CardHeader, Callout, FormGroup, ResponseBox } from '../components/ui';
const { AUTH, INTEL } = ENDPOINTS;
// ── Trust Gauge ───────────────────────────────────────────────────────────────
const TRUST_ARC = 204;
function TrustGauge({ score, label, color }) {
const offset = TRUST_ARC - (score / 100) * TRUST_ARC;
return (
<div style={{ textAlign: 'center' }}>
<svg width="160" height="100" viewBox="-5 -5 170 110" style={{ overflow: 'visible' }}>
<path d="M 10 80 A 65 65 0 0 1 140 80" stroke="#e2e8f0" strokeWidth="16" fill="none" />
<path
d="M 10 80 A 65 65 0 0 1 140 80"
stroke={color || '#16a34a'}
strokeWidth="16"
fill="none"
strokeDasharray={TRUST_ARC}
strokeDashoffset={offset}
strokeLinecap="round"
style={{ transition: 'stroke-dashoffset .6s, stroke .6s' }}
/>
<text x="75" y="77" textAnchor="middle" fontSize="28" fontWeight="800" fill={color || '#16a34a'}>
{Math.round(score)}
</text>
<text x="75" y="94" textAnchor="middle" fontSize="10" fill="#94a3b8">/ 100</text>
</svg>
<div className="trust-label" style={{ color: color || 'var(--muted)' }}>
{(label || 'loading').toUpperCase()}
</div>
</div>
);
}
// ── Behavior bar ──────────────────────────────────────────────────────────────
function BehaviorBar({ label, value }) {
const color = value > 0.7 ? '#16a34a' : value > 0.4 ? '#d97706' : '#dc2626';
return (
<div className="behavior-bar-group">
<div className="behavior-bar-header">
<span>{label}</span>
<span style={{ fontWeight: 700, color }}>{value.toFixed(2)}</span>
</div>
<div className="behavior-bar-track">
<div className="behavior-bar-fill" style={{ width: `${value * 100}%`, background: color }} />
</div>
</div>
);
}
// ── Travel result renderer ────────────────────────────────────────────────────
function TravelResult({ data }) {
if (!data) return null;
const CMAP = { impossible:'#dc2626', suspicious:'#f97316', plausible:'#16a34a', same_area:'#16a34a', coords_unknown:'#94a3b8' };
const IMAP = { impossible:'🚨', suspicious:'⚠️', plausible:'βœ…', same_area:'βœ…', coords_unknown:'❓' };
const col = CMAP[data.verdict] || '#94a3b8';
const icon = IMAP[data.verdict] || '❓';
return (
<div style={{ background: `${col}18`, border: `1px solid ${col}`, borderRadius: 8, padding: 12, marginTop: 10 }}>
<div style={{ fontWeight: 700, fontSize: 15, color: col, marginBottom: 6 }}>
{icon} {(data.verdict || '').toUpperCase().replace('_', ' ')}
</div>
<div className="text-sm">{data.message}</div>
<div className="grid-3 mt-2" style={{ gap: 6 }}>
{[
{ v: data.distance_km || 0, l: 'Distance', u: 'km' },
{ v: Math.round(data.speed_kmh || 0), l: 'Speed', u: 'km/h' },
{ v: Math.round(data.time_gap_minutes || 0), l: 'Gap', u: 'min' },
].map(s => (
<div key={s.l}>
<div style={{ fontWeight: 700 }}>{s.v} {s.u}</div>
<div className="text-xs text-muted">{s.l}</div>
</div>
))}
</div>
{data.trust_delta < 0 && (
<div className="text-sm text-warn mt-2">⚠ Trust impact: {data.trust_delta} pts</div>
)}
</div>
);
}
// ── AI Anomaly result ─────────────────────────────────────────────────────────
function AnomalyResult({ data }) {
if (!data) return null;
return (
<div style={{ background: `${data.color}18`, border: `1px solid ${data.color}`, borderRadius: 8, padding: 12, marginTop: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<div style={{ fontWeight: 800, fontSize: 22, color: data.color }}>{data.anomaly_score.toFixed(1)} / 100</div>
<div style={{ fontSize: 11, fontWeight: 700, background: data.color, color: '#fff', padding: '2px 10px', borderRadius: 4 }}>
{data.classification}
</div>
</div>
<div className="text-xs text-muted mb-2">
Confidence: {(data.confidence * 100).toFixed(0)}% Β· Statistical Isolation Forest
</div>
{Object.entries(data.per_feature || {}).map(([fn, fs]) => {
const fc = fs > 60 ? '#dc2626' : fs > 30 ? '#d97706' : '#16a34a';
return (
<div key={fn} style={{ marginBottom: 6 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11 }}>
<span>{fn}</span><span style={{ color: fc }}>{fs.toFixed(1)}</span>
</div>
<div style={{ height: 5, background: '#e2e8f0', borderRadius: 3, overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${fs}%`, background: fc, borderRadius: 3 }} />
</div>
</div>
);
})}
</div>
);
}
export default function IntelTab({ onTokenSave }) {
// Login status
const [loginStatus, setLoginStatus] = useState('');
// Trust
const [trust, setTrust] = useState({ score: 0, label: 'loading', color: '#94a3b8' });
const [trustHistory, setTrustHist] = useState([]);
const [trustResp, setTrustResp] = useState(null);
// Behavior
const [collecting, setCollecting] = useState(false);
const [bhScores, setBhScores] = useState({ te: 0.5, ml: 0.6, sv: 0.5 });
const [collectStatus,setCollStatus] = useState('');
const [behaviorResp, setBehavResp] = useState(null);
// Travel
const [cities, setCities] = useState([]);
const [travelFrom, setTravelFrom] = useState('New York');
const [travelTo, setTravelTo] = useState('Moscow');
const [travelHours, setTravelHours] = useState('2');
const [travelResult, setTravelResult]= useState(null);
// AI Anomaly
const [aiTyping, setAiTyping] = useState('0.70');
const [aiMouse, setAiMouse] = useState('0.62');
const [aiScroll, setAiScroll] = useState('0.48');
const [aiHour, setAiHour] = useState('0.55');
const [aiFailed, setAiFailed] = useState('0.00');
const [anomResult, setAnomResult]= useState(null);
// Challenge
const [showChallenge, setShowChallenge] = useState(false);
const [challengeQ, setChallengeQ] = useState('');
const [challengeAnswer, setChallengeAnswer] = useState('');
const [challengeMsg, setChallengeMsg] = useState('');
const [challengeId, setChallengeId] = useState(null);
const [challengeResp, setChallengeResp] = useState(null);
// Explain
const [expLoc, setExpLoc] = useState('85');
const [expDev, setExpDev] = useState('15');
const [expTime, setExpTime] = useState('10');
const [expVel, setExpVel] = useState('5');
const [expBeh, setExpBeh] = useState('20');
const [expLevel, setExpLevel] = useState('2');
const [explainResult, setExplainResult] = useState(null);
// Session audit
const [sessionResp, setSessionResp] = useState(null);
// Loading
const [loading, setLoading] = useState({});
const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v }));
// ── Behavior refs ─────────────────────────────────────────────────────────
const bhKeyTimes = useRef([]);
const bhMousePts = useRef([]);
const bhScrollDs = useRef([]);
const bhLastKey = useRef(0);
const bhLastMouse = useRef(0);
const onKeyDown = useCallback(e => {
const now = performance.now();
if (bhLastKey.current > 0) bhKeyTimes.current.push(now - bhLastKey.current);
bhLastKey.current = now;
}, []);
const onMouseMove = useCallback(e => {
const now = performance.now();
if (now - bhLastMouse.current < 50) return;
bhLastMouse.current = now;
bhMousePts.current.push([e.clientX, e.clientY]);
}, []);
const onScroll = useCallback(e => {
const d = e.target?.scrollTop != null ? Math.abs(e.target.scrollTop) : window.scrollY;
bhScrollDs.current.push(d);
}, []);
const computeScores = useCallback(() => {
// Typing entropy (coefficient of variation)
let te = 0.5;
const kt = bhKeyTimes.current;
if (kt.length >= 3) {
const mean = kt.reduce((a, b) => a + b, 0) / kt.length;
const std = Math.sqrt(kt.reduce((s, v) => s + (v - mean) ** 2, 0) / kt.length);
const cv = std / (mean + 1e-6);
te = cv < 0.05 ? 0.10 : cv < 0.20 ? 0.30 + cv * 1.5 : cv < 0.90 ? 0.50 + (cv - 0.20) * 0.7 : Math.max(0.10, 1.0 - (cv - 0.90) * 0.8);
te = Math.max(0, Math.min(1, te));
}
// Mouse linearity
let ml = 0.6;
const mp = bhMousePts.current;
if (mp.length >= 3) {
let totalD = 0;
for (let i = 1; i < mp.length; i++) {
const dx = mp[i][0] - mp[i-1][0], dy = mp[i][1] - mp[i-1][1];
totalD += Math.sqrt(dx * dx + dy * dy);
}
const dxA = mp[mp.length-1][0] - mp[0][0], dyA = mp[mp.length-1][1] - mp[0][1];
const straightD = Math.sqrt(dxA * dxA + dyA * dyA);
ml = Math.max(0, Math.min(1, 1.0 - Math.abs((totalD > 0 ? straightD / totalD : 0) - 0.6) * 1.5));
}
// Scroll variance
let sv = 0.5;
const sd = bhScrollDs.current;
if (sd.length >= 2) {
const sm = sd.reduce((a, b) => a + b, 0) / sd.length;
const ss = Math.sqrt(sd.reduce((s, v) => s + (v - sm) ** 2, 0) / sd.length);
sv = Math.min(1, ss / 200);
}
return { te, ml, sv, lr: Math.max(0, 1.0 - (0.40 * te + 0.35 * ml + 0.25 * sv)) };
}, []);
const startCollecting = () => {
bhKeyTimes.current = []; bhMousePts.current = []; bhScrollDs.current = [];
bhLastKey.current = 0; bhLastMouse.current = 0;
document.addEventListener('keydown', onKeyDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('scroll', onScroll, true);
setCollecting(true);
setCollStatus('Collecting… (type, move mouse, scroll)');
setBhScores({ te: 0.5, ml: 0.6, sv: 0.5 });
};
const stopCollecting = () => {
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('scroll', onScroll, true);
const s = computeScores();
setBhScores(s);
setCollecting(false);
setCollStatus(`Done β€” Keys:${bhKeyTimes.current.length} Mouse:${bhMousePts.current.length} Scrolls:${bhScrollDs.current.length}`);
};
useEffect(() => () => {
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('scroll', onScroll, true);
}, [onKeyDown, onMouseMove, onScroll]);
// ── Cities ────────────────────────────────────────────────────────────────
useEffect(() => {
req(`${INTEL}/demo/city-list`, 'GET', null, false).then(r => {
if (r.ok && r.data?.cities) setCities(r.data.cities.map(c => c.name));
});
if (getToken()) intelGetTrust();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// ── Trust ─────────────────────────────────────────────────────────────────
const intelGetTrust = async () => {
if (!getToken()) return;
setLoad('trust', true);
const r = await req(`${INTEL}/trust-score`);
setTrustResp(r);
if (r.ok && r.data) {
setTrust({ score: r.data.trust_score, label: r.data.label, color: r.data.color });
setTrustHist(r.data.history || []);
}
setLoad('trust', false);
};
const intelVerify = async () => {
if (!getToken()) { alert('Login first.'); return; }
setLoad('verify', true);
const r = await req(`${INTEL}/continuous-verify`, 'POST', {});
setTrustResp(r);
if (r.ok) setTrust({ score: r.data.trust_score, label: r.data.label, color: r.data.color });
setLoad('verify', false);
};
const intelDropTrust = async () => {
if (!getToken()) { alert('Login first (use Quick Login above).'); return; }
setLoad('drop', true);
const r = await req(`${INTEL}/simulate-trust-drop`, 'POST', { target_score: 25, reason: 'Manual demo drop' });
setTrustResp(r);
if (r.ok && r.data) {
setTrust({ score: r.data.new_trust, label: r.data.trust_label, color: r.data.trust_color });
}
setLoad('drop', false);
};
// ── Quick Login ───────────────────────────────────────────────────────────
const quickLogin = async () => {
setLoad('ql', true);
const r = await req(`${AUTH}/login`, 'POST', { email: 'demo.user@adaptive.demo', password: 'DemoUser@123!' }, false);
if (r.ok && r.data?.access_token) {
saveToken(r.data.access_token);
onTokenSave?.();
setLoginStatus('βœ… Logged in as demo.user@adaptive.demo');
await intelGetTrust();
} else {
setLoginStatus('❌ Login failed β€” run Setup in Scenario 1 first.');
}
setLoad('ql', false);
};
// ── Behavior Send ─────────────────────────────────────────────────────────
const sendBehavior = async () => {
if (!getToken()) { alert('Login first.'); return; }
const s = computeScores();
setBhScores(s);
setLoad('beh', true);
const r = await req(`${INTEL}/behavior-signal`, 'POST', {
typing_entropy: s.te, mouse_linearity: s.ml, scroll_variance: s.sv, local_risk_score: s.lr,
});
setBehavResp(r);
if (r.ok && r.data?.trust) {
setTrust({ score: r.data.trust.score, label: r.data.trust.label, color: r.data.trust.color });
}
setLoad('beh', false);
};
// ── Travel ────────────────────────────────────────────────────────────────
const checkTravel = async () => {
setLoad('travel', true);
const r = await req(`${INTEL}/demo/impossible-travel`, 'POST', {
from_city: travelFrom, to_city: travelTo,
time_gap_hours: parseFloat(travelHours), from_country: '', to_country: '',
}, false);
setTravelResult(r.ok ? r.data : null);
setLoad('travel', false);
};
// ── AI Anomaly ────────────────────────────────────────────────────────────
const scoreAnomaly = async () => {
setLoad('ai', true);
const r = await req(`${INTEL}/demo/anomaly-score`, 'POST', {
typing_entropy: parseFloat(aiTyping),
mouse_linearity: parseFloat(aiMouse),
scroll_variance: parseFloat(aiScroll),
hour_normalized: parseFloat(aiHour),
failed_attempts_norm: parseFloat(aiFailed),
}, false);
setAnomResult(r.ok ? r.data : null);
setLoad('ai', false);
};
// ── Micro-Challenge ───────────────────────────────────────────────────────
const generateChallenge = async () => {
if (!getToken()) {
const a = Math.floor(Math.random() * 9) + 2, b = Math.floor(Math.random() * 9) + 2;
setChallengeId('demo-no-auth');
setChallengeQ(`What is ${a} Γ— ${b} ?`);
setChallengeAnswer('');
setChallengeMsg('(Demo mode β€” login to persist trust changes)');
setShowChallenge(true);
return;
}
setLoad('ch', true);
const r = await req(`${INTEL}/micro-challenge/generate`, 'POST', {});
setChallengeResp(r);
if (r.ok && r.data?.challenge) {
setChallengeId(r.data.challenge.challenge_id);
setChallengeQ(r.data.challenge.question);
setChallengeAnswer('');
setChallengeMsg(r.data.challenge_needed ? '' : 'β„Ή Trust is healthy β€” showing challenge for demo purposes.');
setShowChallenge(true);
}
setLoad('ch', false);
};
const verifyChallenge = async () => {
if (!challengeAnswer.trim()) { alert('Enter your answer.'); return; }
if (!challengeId || challengeId === 'demo-no-auth') {
setChallengeMsg('βœ… Submitted (login to update real trust score).');
setShowChallenge(false);
return;
}
setLoad('chv', true);
const r = await req(`${INTEL}/micro-challenge/verify`, 'POST', { challenge_id: challengeId, response: challengeAnswer });
setChallengeResp(r);
if (r.ok && r.data) {
setChallengeMsg(r.data.reason);
if (r.data.correct) {
setShowChallenge(false);
setTrust({ score: r.data.new_trust, label: r.data.trust_label, color: r.data.trust_color });
} else {
setChallengeAnswer('');
}
}
setLoad('chv', false);
};
// ── Explain ───────────────────────────────────────────────────────────────
const explainRisk = async () => {
setLoad('exp', true);
const r = await req(`${INTEL}/demo/explain`, 'POST', {
location_score: parseFloat(expLoc), device_score: parseFloat(expDev),
time_score: parseFloat(expTime), velocity_score: parseFloat(expVel),
behavior_score: parseFloat(expBeh), security_level: parseInt(expLevel),
risk_level: 'medium',
}, false);
setExplainResult(r.ok ? r.data : null);
setLoad('exp', false);
};
return (
<div>
<Callout type="info">
<strong>🧠 Session Intelligence β€” 8 Advanced Security Features</strong><br />
Continuous Verification &bull; Behavioral Intelligence &bull; Dynamic Trust Score &bull;
Micro-Challenges &bull; Explainability &bull; AI Anomaly Detection &bull; Impossible Travel &bull;
Privacy-First Design.
</Callout>
{/* Quick Login */}
<Card>
<CardHeader icon="πŸ”‘">Session Authentication</CardHeader>
<div className="flex items-center gap-3 flex-wrap">
<span className="text-sm text-muted">Protected features require a JWT token.</span>
<button className="btn btn-primary btn-sm" onClick={quickLogin} disabled={loading.ql}>
{loading.ql ? '…' : '⚑ Quick Login (demo user)'}
</button>
{loginStatus && (
<span className="text-sm" style={{ color: loginStatus.startsWith('βœ…') ? 'var(--success)' : 'var(--danger)' }}>
{loginStatus}
</span>
)}
</div>
</Card>
{/* Trust Score */}
<Card>
<CardHeader icon="πŸ›‘οΈ">Dynamic Trust Score &amp; Continuous Verification</CardHeader>
<div className="flex gap-4 flex-wrap items-start">
<TrustGauge score={trust.score} label={trust.label} color={trust.color} />
<div style={{ flex: 1, minWidth: 180 }}>
<div className="flex gap-2 flex-wrap mb-3">
<button className="btn btn-ghost btn-sm" onClick={intelGetTrust} disabled={loading.trust}>πŸ”„ Refresh</button>
<button className="btn btn-ghost btn-sm" onClick={intelVerify} disabled={loading.verify}>βœ” Verify Now</button>
<button className="btn btn-warn btn-sm" onClick={intelDropTrust} disabled={loading.drop}>πŸ”½ Drop to 25</button>
</div>
{trustHistory.length > 0 && (
<div>
<div className="text-xs font-600 uppercase letter-wide text-muted mb-2">Recent Trust Events</div>
<div style={{ maxHeight: 130, overflowY: 'auto' }}>
{[...trustHistory].reverse().slice(0, 20).map((e, i) => (
<div key={i} className="flex justify-between text-xs" style={{ borderBottom: '1px solid var(--border)', padding: '2px 0' }}>
<span className="text-muted">{e.event_type}</span>
<span style={{ color: e.delta >= 0 ? 'var(--success)' : 'var(--danger)' }}>
{e.delta >= 0 ? '+' : ''}{e.delta.toFixed(1)} β†’ {e.score.toFixed(0)}
</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
<ResponseBox result={trustResp} />
</Card>
{/* Behavior Intelligence */}
<Card>
<CardHeader icon="πŸ”’">Privacy-First Behavioral Intelligence</CardHeader>
<div className="callout callout-info text-sm mb-3" style={{ padding: '8px 12px' }}>
<strong>πŸ”’ Privacy-First:</strong> Keystroke timings, mouse coords and scroll deltas are processed{' '}
<em>entirely in-browser</em>. Only the aggregated 0–1 scores are sent to the server.
</div>
<div className="flex gap-2 flex-wrap mb-3 items-center">
<button
className={`btn btn-sm ${collecting ? 'btn-danger' : 'btn-success'}`}
onClick={collecting ? stopCollecting : startCollecting}
>
{collecting ? '⏹ Stop Collecting' : 'β–Ά Start Collecting'}
</button>
<button className="btn btn-primary btn-sm" onClick={sendBehavior} disabled={loading.beh}>
πŸ“€ Send Signals
</button>
{collectStatus && <span className="text-xs text-muted">{collectStatus}</span>}
</div>
<BehaviorBar label="⌨️ Typing Entropy (1.0 = human-like rhythm)" value={bhScores.te} />
<BehaviorBar label="πŸ–±οΈ Mouse Linearity (1.0 = curved/natural)" value={bhScores.ml} />
<BehaviorBar label="πŸ“œ Scroll Variance (0.5 = organic human rhythm)" value={bhScores.sv} />
<ResponseBox result={behaviorResp} />
</Card>
<div className="grid-2">
{/* Impossible Travel */}
<Card style={{ margin: 0 }}>
<CardHeader icon="✈️">Impossible Travel Detector</CardHeader>
<div className="grid-2 mb-2">
<FormGroup label="FROM City">
<select value={travelFrom} onChange={e => setTravelFrom(e.target.value)}>
{cities.map(c => <option key={c}>{c}</option>)}
</select>
</FormGroup>
<FormGroup label="TO City">
<select value={travelTo} onChange={e => setTravelTo(e.target.value)}>
{cities.map(c => <option key={c}>{c}</option>)}
</select>
</FormGroup>
</div>
<FormGroup label="Time gap (hours)">
<input type="number" value={travelHours} onChange={e => setTravelHours(e.target.value)} min="0.01" step="0.5" />
</FormGroup>
<button className="btn btn-primary btn-sm btn-full" onClick={checkTravel} disabled={loading.travel}>
{loading.travel ? '…' : 'πŸ“ Calculate Travel Risk'}
</button>
<TravelResult data={travelResult} />
</Card>
{/* AI Anomaly Scorer */}
<Card style={{ margin: 0 }}>
<CardHeader icon="πŸ€–">AI Anomaly Scorer</CardHeader>
{[
{ label: 'Typing entropy', val: aiTyping, set: setAiTyping },
{ label: 'Mouse linearity', val: aiMouse, set: setAiMouse },
{ label: 'Scroll variance', val: aiScroll, set: setAiScroll },
{ label: 'Hour normalized', val: aiHour, set: setAiHour },
{ label: 'Failed attempts (Γ·20)', val: aiFailed, set: setAiFailed },
].map(f => (
<div key={f.label} className="flex items-center justify-between gap-2 mb-2">
<span className="text-sm text-2">{f.label}</span>
<input
type="number" value={f.val} onChange={e => f.set(e.target.value)}
min="0" max="1" step="0.05"
style={{ width: 72, textAlign: 'right', padding: '3px 6px' }}
/>
</div>
))}
<button className="btn btn-primary btn-sm btn-full mt-2" onClick={scoreAnomaly} disabled={loading.ai}>
{loading.ai ? '…' : '🧠 Score with AI'}
</button>
<AnomalyResult data={anomResult} />
</Card>
</div>
{/* Micro-Challenges */}
<Card>
<CardHeader icon="🧩">Low-Friction Micro-Challenges</CardHeader>
<p className="text-sm text-muted mb-3">
Challenges fire <em>only when trust drops below 40</em> β€” never interrupts a trusted session.
</p>
<div className="flex gap-2 flex-wrap mb-3">
<button className="btn btn-warn btn-sm" onClick={intelDropTrust} disabled={loading.drop}>πŸ”½ Drop Trust to 25</button>
<button className="btn btn-primary btn-sm" onClick={generateChallenge} disabled={loading.ch}>🧩 Generate Challenge</button>
</div>
{showChallenge && (
<div className="callout callout-info">
<div className="font-bold mb-2" style={{ fontSize: 16 }}>{challengeQ}</div>
<div className="flex gap-2 items-center">
<input
type="text"
value={challengeAnswer}
onChange={e => setChallengeAnswer(e.target.value)}
placeholder="Your answer…"
style={{ width: 160 }}
/>
<button className="btn btn-success btn-sm" onClick={verifyChallenge} disabled={loading.chv}>
{loading.chv ? '…' : 'βœ” Verify'}
</button>
</div>
{challengeMsg && <div className="text-sm mt-2">{challengeMsg}</div>}
</div>
)}
<ResponseBox result={challengeResp} />
</Card>
{/* Explainability */}
<Card>
<CardHeader icon="πŸ“Š">Explainable Risk Transparency</CardHeader>
<p className="text-sm text-muted mb-3">
Submit factor scores and see exactly which signals contributed and why β€” with model weights.
</p>
<div className="grid-3 mb-3">
{[
{ label: '🌍 Location (0-100)', val: expLoc, set: setExpLoc, max: 100 },
{ label: 'πŸ’» Device', val: expDev, set: setExpDev, max: 100 },
{ label: 'πŸ• Time', val: expTime, set: setExpTime, max: 100 },
{ label: '⚑ Velocity', val: expVel, set: setExpVel, max: 100 },
{ label: '🧠 Behavior', val: expBeh, set: setExpBeh, max: 100 },
{ label: 'πŸ”’ Security level (0-4)',val: expLevel, set: setExpLevel, max: 4 },
].map(f => (
<FormGroup key={f.label} label={f.label}>
<input type="number" value={f.val} onChange={e => f.set(e.target.value)} min="0" max={f.max} />
</FormGroup>
))}
</div>
<button className="btn btn-primary btn-sm" onClick={explainRisk} disabled={loading.exp}>
{loading.exp ? '…' : 'πŸ” Generate Explanation'}
</button>
{explainResult && (
<div className="mt-3">
<div className="text-sm text-muted mb-2">
πŸ” Audit ID: <code>{explainResult.audit_id}</code> &nbsp;Β·&nbsp;
Confidence: {(explainResult.confidence * 100).toFixed(0)}% &nbsp;Β·&nbsp;
Action: <em>{explainResult.action}</em>
</div>
<div className="resp-box" style={{ background: 'var(--surface-2)' }}>{explainResult.summary}</div>
{(explainResult.factors || []).map(f => {
const col = f.status === 'anomalous' ? '#dc2626' : '#16a34a';
const bar = Math.min(100, Math.max(0, Math.abs(f.contribution) * 4));
return (
<div key={f.factor} className="factor-row mt-2">
<div className="factor-label">
<span>{f.icon} <strong>{f.factor}</strong> <span className="text-xs text-muted">w:{f.model_weight}</span></span>
<span style={{ color: col }}>{f.contribution >= 0 ? '+' : ''}{f.contribution.toFixed(1)}</span>
</div>
<div className="factor-bar-wrap">
<div className="factor-bar" style={{ width: `${bar}%`, background: col }} />
</div>
<div className="text-xs text-muted mt-1">{f.detail}</div>
</div>
);
})}
</div>
)}
</Card>
{/* Session Audit Trail */}
<Card>
<CardHeader icon="πŸ“‹">Session Audit Trail <span className="text-sm text-muted font-400">(requires login)</span></CardHeader>
<button className="btn btn-ghost btn-sm mb-2" onClick={async () => {
if (!getToken()) { alert('Login first.'); return; }
setLoad('audit', true);
setSessionResp(await req(`${INTEL}/explain`));
setLoad('audit', false);
}} disabled={loading.audit}>
{loading.audit ? '…' : 'πŸ“„ Fetch My Session Events'}
</button>
<ResponseBox result={sessionResp} />
</Card>
</div>
);
}