// H2Learn — Network Hero Screen
// Six modes: Atlas / Orbital / Focus / Anti-dup / Need-Offer / Path
// Filters · detail drawer · why-connected · graph stats · legend
(function() {
const D = window.H2Data;
const L = window.GraphLayouts;
const { useState, useMemo } = React;
const MODES = [
{ key:'atlas', label:'Atlas', icon:'network', desc:'Force-directed clusters' },
{ key:'orbital', label:'Orbital', icon:'target', desc:'Ecosystem rings' },
{ key:'focus', label:'Focus', icon:'compass', desc:'My local network' },
{ key:'antidup', label:'Anti-duplication', icon:'alert', desc:'Overlap detection' },
{ key:'needoffer', label:'Need ↔ Offer', icon:'git', desc:'Sankey-style flow' },
{ key:'path', label:'Path', icon:'arrowRight', desc:'Connection chain' },
];
const ENTITY_FILTERS = [
{ key:'person', label:'People', color:'var(--c-person)' },
{ key:'project',label:'Projects', color:'var(--c-project)' },
{ key:'org', label:'Orgs', color:'var(--c-org)' },
{ key:'domain', label:'Domains', color:'var(--c-domain)' },
{ key:'method', label:'Methods', color:'var(--c-method)' },
{ key:'challenge',label:'Challenges',color:'var(--c-challenge)' },
];
const REL_FILTERS = [
'Similarity','Complementarity','Anti-duplication','Bridge',
'Method transfer','Shared uncertainty','Need-offer','Project compatibility',
];
const NetworkScreen = () => {
const [mode, setMode] = useState('atlas');
const [selectedNode, setSelectedNode] = useState('p_sara');
const [hoveredNode, setHoveredNode] = useState(null);
const [selectedEdge, setSelectedEdge] = useState(null);
const [drawerView, setDrawerView] = useState('node'); // node | edge | why
const [strengthMin, setStrengthMin] = useState(0.3);
const [enabledTypes, setEnabledTypes] = useState(new Set(['person','project','org','domain','method']));
const [enabledRels, setEnabledRels] = useState(new Set(REL_FILTERS));
const [dimNonAdjacent, setDimNonAdjacent] = useState(false);
// Compose graph data per mode
const graph = useMemo(() => {
if (mode === 'atlas') return { nodes: L.ATLAS.nodes, edges: L.ATLAS.edges, clusters: L.ATLAS.clusters };
if (mode === 'orbital') {
const placed = L.placeOrbital(L.ORBITAL);
return { nodes: Object.values(placed), edges: L.ORBITAL.edges, rings: L.ORBITAL.rings, ringCenter: { cx: L.ORBITAL.cx, cy: L.ORBITAL.cy } };
}
if (mode === 'focus') {
const f = L.buildFocus(selectedNode || 'p_sara');
return { nodes: f.nodes, edges: f.edges };
}
if (mode === 'antidup') return { nodes: L.ANTIDUP.nodes, edges: L.ANTIDUP.edges, clusters: L.ANTIDUP.clusters };
if (mode === 'path') {
const f = L.buildPath([
{ id:'p_sara', kind:'person', initials:'SA', label:'You · Sara', edgeReason:'Has expertise in', relationTo:'Similarity' },
{ id:'d_pem', kind:'domain', initials:'P', label:'PEM', edgeReason:'Worked on by', relationTo:'Similarity' },
{ id:'p_liesbeth', kind:'person', initials:'LK', label:'Liesbeth', edgeReason:'Member of', relationTo:'Similarity' },
{ id:'o_tno', kind:'org', initials:'TN', label:'TNO', edgeReason:'Partner in', relationTo:'Similarity' },
{ id:'pj_hydronl', kind:'project', initials:'HN', label:'HYDRO-NL', edgeReason:'Led by', relationTo:'Similarity' },
]);
return { nodes: f.nodes, edges: f.edges, viewBox: '0 0 1100 720' };
}
return { nodes: [], edges: [] };
}, [mode, selectedNode]);
// Filter
const filtered = useMemo(() => {
if (mode === 'needoffer') return graph;
const ns = graph.nodes.filter(n => !enabledTypes.size || enabledTypes.has(n.kind));
const ids = new Set(ns.map(n => n.id));
const es = graph.edges.filter(e =>
ids.has(e.s) && ids.has(e.t) &&
enabledRels.has(e.type) &&
(e.strength || 0) >= strengthMin
);
return { ...graph, nodes: ns, edges: es };
}, [graph, enabledTypes, enabledRels, strengthMin, mode]);
const onNodeClick = (n) => {
setSelectedNode(n.id); setSelectedEdge(null); setDrawerView('node');
if (mode === 'focus') {/* keep focus */}
};
const onEdgeClick = (e) => {
setSelectedEdge(e); setDrawerView('edge');
};
const stats = useMemo(() => ({
nodes: filtered.nodes?.length || 0,
edges: filtered.edges?.length || 0,
clusters: filtered.clusters?.length || 4,
recommended: (filtered.edges || []).filter(e => e.recommended).length,
antidup: (filtered.edges || []).filter(e => e.alert).length,
}), [filtered]);
return (
{/* ============ FILTER PANEL (left) ============ */}
{/* ============ CANVAS (center) ============ */}
{/* mode bar */}
{MODES.map(m => (
))}
{/* canvas + overlays */}
{mode === 'needoffer' ? (
{ setSelectedEdge({matchId:'m_01', type:'Complementarity', s:'p_sara', t:'pj_hydronl', strength: 0.88}); setDrawerView('edge'); }}/>
) : (
{mode === 'antidup' && }
>}
/>
)}
{/* ============ DETAIL DRAWER (right) ============ */}
);
};
// ============ Filter panel ============
const FilterPanel = ({mode, enabledTypes, setEnabledTypes, enabledRels, setEnabledRels, strengthMin, setStrengthMin, dimNonAdjacent, setDimNonAdjacent}) => {
const toggleSet = (set, setter, key) => {
const next = new Set(set);
if (next.has(key)) next.delete(key); else next.add(key);
setter(next);
};
return (
Network
Ecosystem map
248 nodes · 134 edges across 4 clusters
{ENTITY_FILTERS.map(t => (
toggleSet(enabledTypes, setEnabledTypes, t.key)}
swatch={}
label={t.label}
/>
))}
{REL_FILTERS.map(r => {
const meta = window.RelMeta[r] || window.RelMeta.Similarity;
return (
toggleSet(enabledRels, setEnabledRels, r)}
swatch={}
label={r}
/>
);
})}
setDimNonAdjacent(!dimNonAdjacent)} label="Dim non-adjacent"/>
View presets
My ecosystem
Anti-dup risks
Bridge actors
);
};
const FilterGroup = ({title, children}) => (
);
const FilterRow = ({on=true, onClick, swatch, label}) => (
);
const Swatch = ({type, color}) => {
if (type === 'project') return ;
if (type === 'org') return ;
return ;
};
const EdgeSwatch = ({type, color}) => {
const dash = type === 'Anti-duplication' ? '4 3' : type === 'Shared uncertainty' ? '2 4' : type === 'Method transfer' ? '3 2' : null;
return (
);
};
const ContextFilter = ({label, options}) => (
{label}
);
// ============ Mode badge ============
const ModeBadge = ({mode}) => {
const m = MODES.find(x => x.key === mode);
return (
);
};
// ============ Legend ============
const Legend = () => {
const items = [
{ kind:'Person', color:'var(--c-person)', shape:'circle'},
{ kind:'Project', color:'var(--c-project)', shape:'hex'},
{ kind:'Org', color:'var(--c-org)', shape:'square'},
{ kind:'Domain', color:'var(--c-domain)', shape:'circle'},
{ kind:'Method', color:'var(--c-method)', shape:'circle'},
];
const edges = [
{ type:'Similarity', color:'var(--e-similar)' },
{ type:'Complementarity',color:'var(--e-comp)' },
{ type:'Anti-dup', color:'var(--e-antidup)', dash:'4 3' },
{ type:'Bridge', color:'var(--e-bridge)' },
{ type:'Method', color:'var(--e-method)', dash:'3 2' },
];
return (
Legend
{items.map(i => (
{i.kind}
))}
{edges.map(e => (
{e.type}
))}
);
};
const GraphStats = ({stats}) => (
);
const Stat = ({v, l, highlight}) => (
);
const ZoomControls = () => (
);
const AntiDupBanner = () => (
3 anti-duplication clusters detected
· membrane protocols ×3 · pilot data ×2 · safety curriculum ×2
Review
);
// ============ Need-Offer view ============
const NeedOfferView = ({onMatch}) => {
const { needs, offers, flows } = L.NEED_OFFER;
const W = 1200, H = 620;
const colW = 280;
const lx = 60, rx = W - colW - 60;
const yStep = 70, yTop = 100;
const needPos = needs.reduce((m, n, i) => { m[n.id] = { x: lx + colW, y: yTop + i*yStep }; return m; }, {});
const offerPos = offers.reduce((m, o, i) => { m[o.id] = { x: rx, y: yTop + i*yStep - 30 }; return m; }, {});
return (
);
};
// ============ Detail drawer (right) ============
const DetailDrawer = ({view, setView, selectedNode, setSelectedNode, selectedEdge, mode}) => {
const node = D.byId(selectedNode);
const kind = D.kindOf(selectedNode || 'p_sara');
const match = selectedEdge && selectedEdge.matchId ? D.MATCHES.find(m => m.id === selectedEdge.matchId) : null;
return (
{view === 'node' && node && }
{view === 'edge' && selectedEdge && }
{view === 'why' && (match || selectedEdge) && }
{view === 'why' && !selectedEdge && !match && (
)}
Send signal
location.hash = 'match'}/>
);
};
const NodeDetail = ({node, kind, setView}) => (
{kind === 'person' ? 'Person' : kind === 'project' ? 'Project' : 'Organization'}
{node.name}
{node.role || node.type} · {node.org || node.region}
{node.bio &&
{node.bio}
}
{/* fingerprint */}
{node.fingerprint && (
)}
{/* project context */}
{node.trl != null && (
Project context
Stage: {node.stage}
TRL {node.trl}
{node.scale}
{node.region}
)}
{/* needs/offers */}
{(node.needs || node.offers) && (
{node.needs && (
Needs
{node.needs.map((n,i) => {n})}
)}
{node.offers && (
Offers
{node.offers.map((o,i) => {o})}
)}
)}
{/* bottlenecks / uncertainties */}
{(node.bottlenecks || node.uncertainties) && (
Blockers & unknowns
{node.bottlenecks && node.bottlenecks.map((b,i) => (
{b.label}
severity {b.severity}/5
))}
{node.uncertainties && node.uncertainties.map((u,i) => (
))}
)}
{/* top connections */}
Top connections
{[
{n: D.PEOPLE[1], why:'Similarity · 92%', rel:'Similarity'},
{n: D.PROJECTS[0], why:'Complementarity · 88%', rel:'Complementarity'},
{n: D.PEOPLE[3], why:'Shared uncertainty · 71%', rel:'Shared uncertainty'},
].map((c,i) => (
))}
{/* actions */}
Open full profile
Compare
Focus here
);
const EdgeDetail = ({edge, match, setView}) => {
if (!edge) return No edge selected
;
const a = D.byId(edge.s), b = D.byId(edge.t);
return (
Strength
{Math.round((edge.strength || 0)*100)}%
Confidence
{Math.round((match?.confidence || 0.85)*100)}%
Source
{match?.curated ? 'Curated' : 'System'}
{match && (
Why connected — short
setView('why')}>See full explanation
)}
location.hash = 'match'} style={{justifyContent:'center'}}>Open match detail
);
};
const WhyConnected = ({edge, match}) => {
if (!match) return Select an edge to see why it exists
;
return (
Why connected?
{match.summary}
Signals · contribution to score
{match.signals.map((s, i) => (
{s.name}
{s.score}%
{s.expl}
source · {s.source}
))}
location.hash = 'match'} style={{justifyContent:'center'}}>Open full match detail
);
};
Object.assign(window, { NetworkScreen });
})();