Spaces:
Sleeping
Sleeping
Download frontend/src/pages/Simulate.jsx from shield137/shockmap-api: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/spaces/shield137/shockmap-api/resolve/main/frontend/src/pages/Simulate.jsx
- Command line
-
hf download hf://spaces/shield137/shockmap-api/frontend/src/pages/Simulate.jsx
-
curl -L -o Simulate.jsx https://huggingface.co/spaces/shield137/shockmap-api/resolve/main/frontend/src/pages/Simulate.jsx
16.1 kB
| import { useState, useEffect } from 'react'; | |
| import { api } from '../api/client'; | |
| const SEVERITIES = [ | |
| { value: 'warning', label: 'Warning', desc: 'Early signs, low impact', factor: 0.3 }, | |
| { value: 'partial_shutdown', label: 'Partial Shutdown', desc: '30–70% capacity loss', factor: 0.6 }, | |
| { value: 'full_shutdown', label: 'Full Shutdown', desc: '100% production halt', factor: 1.0 }, | |
| ]; | |
| const COMM_COLORS = ['#818cf8', '#34d399', '#f472b6', '#fbbf24', '#38bdf8', '#a78bfa']; | |
| function riskColor(score) { | |
| if (score >= 75) return '#f43f5e'; | |
| if (score >= 50) return '#f59e0b'; | |
| if (score >= 25) return '#60a5fa'; | |
| return '#10b981'; | |
| } | |
| function RiskBar({ score }) { | |
| const color = riskColor(score); | |
| return ( | |
| <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> | |
| <div style={{ flex: 1, height: 6, background: 'rgba(255,255,255,0.06)', borderRadius: 4, overflow: 'hidden' }}> | |
| <div style={{ height: '100%', width: `${Math.min(100, score)}%`, background: color, borderRadius: 4, transition: 'width 0.6s ease' }} /> | |
| </div> | |
| <span style={{ fontSize: '0.72rem', color, fontWeight: 600, minWidth: 32, textAlign: 'right' }}> | |
| {score.toFixed(0)} | |
| </span> | |
| </div> | |
| ); | |
| } | |
| export default function Simulate() { | |
| const [provinces, setProvinces] = useState([]); | |
| const [province, setProvince] = useState(''); | |
| const [duration, setDuration] = useState(30); | |
| const [severity, setSeverity] = useState('partial_shutdown'); | |
| const [result, setResult] = useState(null); | |
| const [propagation, setPropagation] = useState(null); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState(null); | |
| const [graphLoading, setGraphLoading] = useState(true); | |
| useEffect(() => { | |
| api.getGraph() | |
| .then(g => { | |
| const ps = (g.nodes || []).filter(n => n.type === 'province'); | |
| setProvinces(ps); | |
| if (ps.length) setProvince(ps[0].id); | |
| }) | |
| .catch(() => {}) | |
| .finally(() => setGraphLoading(false)); | |
| }, []); | |
| async function run() { | |
| if (!province) return; | |
| setLoading(true); | |
| setError(null); | |
| setResult(null); | |
| setPropagation(null); | |
| try { | |
| const [simRes, propRes] = await Promise.all([ | |
| api.simulate(province, Number(duration), severity), | |
| api.getPropagation(province).catch(() => null), | |
| ]); | |
| setResult(simRes); | |
| setPropagation(propRes); | |
| } catch (e) { | |
| setError(e.message); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // Merge simulate result + propagation data for richer display | |
| const affectedWithPR = result?.affected_drugs?.map(drug => { | |
| const prData = propagation?.affected_nodes?.[drug.id]; | |
| return { | |
| ...drug, | |
| risk: drug.current_risk || 0, | |
| pagerank: prData?.components?.pagerank_normalized ?? null, | |
| buffer_days: prData?.components?.buffer_days ?? null, | |
| substitutability: prData?.components?.substitutability ?? null, | |
| community_label: prData?.components?.community_label ?? null, | |
| community_amp: prData?.components?.community_amplifier ?? null, | |
| }; | |
| }) || []; | |
| return ( | |
| <div style={{ padding: '28px 32px', maxWidth: 900, animation: 'fade-in 0.35s ease' }}> | |
| <div style={{ marginBottom: 28 }}> | |
| <h1 style={{ fontSize: '1.35rem', fontWeight: 700, letterSpacing: '-0.03em', marginBottom: 4 }}> | |
| Shock Simulator | |
| </h1> | |
| <p style={{ fontSize: '0.8rem', color: 'var(--muted)' }}> | |
| Model downstream impact via Personalized PageRank ·{' '} | |
| <span style={{ fontFamily: 'monospace', fontSize: '0.72rem', color: 'var(--muted)' }}> | |
| R = PR(shock) × (1-S) × e^(−B/τ) × C | |
| </span> | |
| </p> | |
| </div> | |
| {/* Config Card */} | |
| <div className="card" style={{ padding: '22px 24px', marginBottom: 20 }}> | |
| {/* Province */} | |
| <div style={{ marginBottom: 18 }}> | |
| <label style={{ display: 'block', fontSize: '0.72rem', color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}> | |
| Shock Origin — Province / Region | |
| </label> | |
| {graphLoading ? ( | |
| <div className="skeleton" style={{ height: 38 }} /> | |
| ) : ( | |
| <select | |
| value={province} | |
| onChange={e => setProvince(e.target.value)} | |
| style={{ | |
| width: '100%', padding: '9px 12px', fontSize: '0.85rem', | |
| background: 'var(--surface2)', border: '1px solid var(--border2)', | |
| borderRadius: 8, color: 'var(--text)', outline: 'none', | |
| }} | |
| > | |
| {provinces.length === 0 && <option value="">No provinces loaded</option>} | |
| {provinces.map(p => ( | |
| <option key={p.id} value={p.id}>{p.name || p.id}</option> | |
| ))} | |
| </select> | |
| )} | |
| </div> | |
| {/* Duration */} | |
| <div style={{ marginBottom: 18 }}> | |
| <label style={{ display: 'block', fontSize: '0.72rem', color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}> | |
| Duration: <span style={{ color: 'var(--text)', fontWeight: 600 }}>{duration} days</span> | |
| <span style={{ marginLeft: 8, color: 'var(--primary)', fontSize: '0.65rem' }}> | |
| τ = 30d (pharma) · duration factor = {Math.min(1, duration / 30).toFixed(2)} | |
| </span> | |
| </label> | |
| <input type="range" min={1} max={180} step={1} value={duration} | |
| onChange={e => setDuration(Number(e.target.value))} | |
| style={{ width: '100%', accentColor: 'var(--primary)' }} | |
| /> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.68rem', color: 'var(--muted)', marginTop: 4 }}> | |
| <span>1 day</span><span>90 days</span><span>180 days</span> | |
| </div> | |
| </div> | |
| {/* Severity */} | |
| <div style={{ marginBottom: 22 }}> | |
| <label style={{ display: 'block', fontSize: '0.72rem', color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}> | |
| Disruption Severity | |
| </label> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}> | |
| {SEVERITIES.map(s => ( | |
| <button key={s.value} onClick={() => setSeverity(s.value)} | |
| style={{ | |
| padding: '12px 14px', borderRadius: 10, border: '1px solid', | |
| borderColor: severity === s.value ? 'var(--primary)' : 'var(--border2)', | |
| background: severity === s.value ? 'rgba(79,156,249,0.1)' : 'var(--surface2)', | |
| textAlign: 'left', cursor: 'pointer', transition: 'all 0.15s', | |
| }} | |
| > | |
| <p style={{ fontSize: '0.82rem', fontWeight: 600, color: 'var(--text)', marginBottom: 2 }}>{s.label}</p> | |
| <p style={{ fontSize: '0.68rem', color: 'var(--muted)' }}>{s.desc}</p> | |
| <p style={{ fontSize: '0.65rem', color: 'var(--primary)', marginTop: 4 }}> | |
| severity × {s.factor} | |
| </p> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <button onClick={run} disabled={loading || !province} | |
| style={{ | |
| width: '100%', padding: '12px', background: loading ? 'var(--surface2)' : 'var(--primary)', | |
| color: loading ? 'var(--muted)' : '#fff', border: 'none', borderRadius: 10, | |
| fontSize: '0.85rem', fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer', | |
| transition: 'all 0.2s', | |
| }} | |
| > | |
| {loading ? '⏳ Running PageRank simulation…' : '⚡ Run Simulation'} | |
| </button> | |
| </div> | |
| {error && ( | |
| <div style={{ padding: '14px 18px', background: 'rgba(244,63,94,0.08)', border: '1px solid rgba(244,63,94,0.2)', borderRadius: 10, color: '#f43f5e', fontSize: '0.82rem', marginBottom: 20 }}> | |
| {error} | |
| </div> | |
| )} | |
| {result && ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}> | |
| {/* Propagation Explanation */} | |
| <div className="card" style={{ padding: '20px 22px' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}> | |
| <span style={{ fontWeight: 600, fontSize: '0.88rem' }}>📊 Propagation Analysis</span> | |
| <span style={{ | |
| fontSize: '0.62rem', background: 'rgba(244,114,182,0.12)', color: '#f472b6', | |
| border: '1px solid rgba(244,114,182,0.25)', borderRadius: 999, padding: '2px 7px', | |
| }}> | |
| Personalized PageRank | |
| </span> | |
| </div> | |
| <p style={{ fontSize: '0.82rem', color: 'var(--text)', lineHeight: 1.6, marginBottom: 10 }}> | |
| {result.propagation_explanation} | |
| </p> | |
| {propagation && ( | |
| <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', fontSize: '0.72rem', color: 'var(--muted)' }}> | |
| <span>Total affected: <strong style={{ color: 'var(--text)' }}>{propagation.total_affected}</strong> nodes</span> | |
| <span>Method: <strong style={{ color: 'var(--text)' }}>{propagation.method}</strong></span> | |
| <span>Community cluster: <strong style={{ color: '#818cf8' }}>#{propagation.propagation_trace?.origin_community ?? 'N/A'}</strong></span> | |
| </div> | |
| )} | |
| <p style={{ fontSize: '0.68rem', color: 'var(--muted)', marginTop: 8 }}> | |
| Simulated at {new Date(result.simulated_at).toLocaleString('en-IN')} | |
| </p> | |
| </div> | |
| {/* Affected Drugs — with PageRank breakdown */} | |
| {affectedWithPR.length > 0 ? ( | |
| <div className="card" style={{ padding: '20px 22px' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}> | |
| <span style={{ fontWeight: 600, fontSize: '0.88rem' }}> | |
| Top {affectedWithPR.length} Affected Inputs | |
| </span> | |
| <span style={{ | |
| fontSize: '0.62rem', background: 'rgba(251,191,36,0.12)', color: '#fbbf24', | |
| border: '1px solid rgba(251,191,36,0.25)', borderRadius: 999, padding: '2px 7px', | |
| }}> | |
| R = PR × (1−S) × e^(−B/τ) × C | |
| </span> | |
| </div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> | |
| {affectedWithPR.map((drug, i) => ( | |
| <div key={drug.id} style={{ | |
| padding: '12px 14px', background: 'var(--surface2)', | |
| border: '1px solid var(--border2)', borderRadius: 10, | |
| }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: drug.pagerank !== null ? 8 : 0 }}> | |
| <div style={{ flex: 1, minWidth: 0 }}> | |
| <p style={{ fontSize: '0.82rem', fontWeight: 600, color: 'var(--text)', marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> | |
| {drug.name} | |
| </p> | |
| <span style={{ fontSize: '0.68rem', color: 'var(--muted)' }}>{drug.therapeutic_class || drug.nlem_tier}</span> | |
| </div> | |
| <div style={{ minWidth: 160 }}> | |
| <RiskBar score={drug.risk} /> | |
| </div> | |
| </div> | |
| {/* PageRank component breakdown */} | |
| {drug.pagerank !== null && ( | |
| <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 6 }}> | |
| <span style={{ | |
| fontSize: '0.65rem', color: '#f472b6', | |
| background: 'rgba(244,114,182,0.1)', border: '1px solid rgba(244,114,182,0.2)', | |
| borderRadius: 999, padding: '2px 8px', | |
| }}> | |
| PR: {(drug.pagerank * 100).toFixed(1)}% | |
| </span> | |
| {drug.buffer_days !== null && ( | |
| <span style={{ | |
| fontSize: '0.65rem', color: '#fbbf24', | |
| background: 'rgba(251,191,36,0.1)', border: '1px solid rgba(251,191,36,0.2)', | |
| borderRadius: 999, padding: '2px 8px', | |
| }}> | |
| Buffer: {drug.buffer_days}d | |
| </span> | |
| )} | |
| {drug.substitutability !== null && ( | |
| <span style={{ | |
| fontSize: '0.65rem', color: '#60a5fa', | |
| background: 'rgba(96,165,250,0.1)', border: '1px solid rgba(96,165,250,0.2)', | |
| borderRadius: 999, padding: '2px 8px', | |
| }}> | |
| Sub: {(drug.substitutability * 100).toFixed(0)}% | |
| </span> | |
| )} | |
| {drug.community_label && ( | |
| <span style={{ | |
| fontSize: '0.65rem', color: COMM_COLORS[i % COMM_COLORS.length], | |
| background: `${COMM_COLORS[i % COMM_COLORS.length]}12`, | |
| border: `1px solid ${COMM_COLORS[i % COMM_COLORS.length]}30`, | |
| borderRadius: 999, padding: '2px 8px', | |
| }}> | |
| {drug.community_label} | |
| </span> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="card" style={{ padding: '32px', textAlign: 'center' }}> | |
| <p style={{ fontSize: '0.85rem', color: 'var(--muted)' }}> | |
| No inputs significantly affected at this severity level | |
| </p> | |
| </div> | |
| )} | |
| {/* Community Propagation Summary */} | |
| {propagation?.propagation_trace?.propagation_edges?.length > 0 && ( | |
| <div className="card" style={{ padding: '20px 22px' }}> | |
| <span style={{ fontWeight: 600, fontSize: '0.88rem', display: 'block', marginBottom: 14 }}> | |
| 🔗 Supply Chain Propagation Paths | |
| </span> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> | |
| {propagation.propagation_trace.propagation_edges.slice(0, 8).map((edge, i) => ( | |
| <div key={i} style={{ | |
| display: 'flex', alignItems: 'center', gap: 10, fontSize: '0.75rem', | |
| padding: '8px 12px', background: 'var(--surface2)', borderRadius: 8, | |
| }}> | |
| <span style={{ color: 'var(--muted)', minWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> | |
| {edge.from} | |
| </span> | |
| <span style={{ color: 'var(--border2)' }}>→</span> | |
| <span style={{ color: 'var(--text)', flex: 1 }}>{edge.name}</span> | |
| <span style={{ | |
| fontSize: '0.65rem', color: 'var(--primary)', | |
| background: 'rgba(79,156,249,0.1)', border: '1px solid rgba(79,156,249,0.2)', | |
| borderRadius: 999, padding: '1px 7px', | |
| }}> | |
| w={edge.weight.toFixed ? edge.weight.toFixed(2) : edge.weight} | |
| </span> | |
| <span style={{ fontSize: '0.62rem', color: 'var(--muted)' }}>{edge.type}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |