Spaces:
Running
Running
| 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 • Behavioral Intelligence • Dynamic Trust Score • | |
| Micro-Challenges • Explainability • AI Anomaly Detection • Impossible Travel • | |
| 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 & 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> Β· | |
| Confidence: {(explainResult.confidence * 100).toFixed(0)}% Β· | |
| 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> | |
| ); | |
| } | |