Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
1
vendor/ruvector/examples/rvf/dashboard/dist/assets/d3-BwsdWUnD.js
vendored
Normal file
1
vendor/ruvector/examples/rvf/dashboard/dist/assets/d3-BwsdWUnD.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/ruvector/examples/rvf/dashboard/dist/assets/index-BfCA2--q.css
vendored
Normal file
1
vendor/ruvector/examples/rvf/dashboard/dist/assets/index-BfCA2--q.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1288
vendor/ruvector/examples/rvf/dashboard/dist/assets/index-DTWFqlsC.js
vendored
Normal file
1288
vendor/ruvector/examples/rvf/dashboard/dist/assets/index-DTWFqlsC.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3828
vendor/ruvector/examples/rvf/dashboard/dist/assets/three-Cdmz7B_B.js
vendored
Normal file
3828
vendor/ruvector/examples/rvf/dashboard/dist/assets/three-Cdmz7B_B.js
vendored
Normal file
File diff suppressed because one or more lines are too long
200
vendor/ruvector/examples/rvf/dashboard/dist/index.html
vendored
Normal file
200
vendor/ruvector/examples/rvf/dashboard/dist/index.html
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Causal Atlas</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; width: 100%; overflow: hidden; }
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: #0B0F14;
|
||||
color: #E6EDF3;
|
||||
}
|
||||
#shell {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
/* Left nav rail */
|
||||
#nav-rail {
|
||||
width: 72px;
|
||||
min-width: 72px;
|
||||
background: #0B0F14;
|
||||
border-right: 1px solid #1E2630;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 16px;
|
||||
gap: 2px;
|
||||
z-index: 20;
|
||||
}
|
||||
#nav-rail a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 52px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: #8B949E;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
#nav-rail a svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
#nav-rail a span {
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
margin-top: 3px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
#nav-rail a:hover {
|
||||
color: #E6EDF3;
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
#nav-rail a.active {
|
||||
color: #00E5FF;
|
||||
background: rgba(0,229,255,0.08);
|
||||
}
|
||||
/* Main area */
|
||||
#main-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
/* Top bar */
|
||||
#top-bar {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
background: #0B0F14;
|
||||
border-bottom: 1px solid #1E2630;
|
||||
}
|
||||
#top-bar .sys-name {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
color: #E6EDF3;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
#top-bar .hash-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
#top-bar .hash-display .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #2ECC71;
|
||||
}
|
||||
#top-bar .hash-display code {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace;
|
||||
font-size: 11px;
|
||||
color: #8B949E;
|
||||
}
|
||||
/* App container */
|
||||
#app {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-DTWFqlsC.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/three-Cdmz7B_B.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/d3-BwsdWUnD.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BfCA2--q.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="shell">
|
||||
<nav id="nav-rail">
|
||||
<a href="#/atlas" title="Atlas Explorer">
|
||||
<svg viewBox="0 0 24 24"><circle cx="5" cy="5" r="1.5"/><circle cx="19" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="5" cy="19" r="1.5"/><circle cx="19" cy="19" r="1.5"/><line x1="5" y1="5" x2="12" y2="12"/><line x1="19" y1="5" x2="12" y2="12"/><line x1="12" y1="12" x2="5" y2="19"/><line x1="12" y1="12" x2="19" y2="19"/></svg>
|
||||
<span>Atlas</span>
|
||||
</a>
|
||||
<a href="#/coherence" title="Coherence Field">
|
||||
<svg viewBox="0 0 24 24"><path d="M2 12c2-3 4-5 6-5s4 5 6 5 4-5 6-5"/><path d="M2 17c2-3 4-5 6-5s4 5 6 5 4-5 6-5" opacity="0.5"/></svg>
|
||||
<span>Coherence</span>
|
||||
</a>
|
||||
<a href="#/boundaries" title="Boundary Evolution">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
<span>Bounds</span>
|
||||
</a>
|
||||
<a href="#/memory" title="Memory Tiers">
|
||||
<svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="14" x2="20" y2="14"/><circle cx="8" cy="6.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="8" cy="11.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="8" cy="16.5" r="0.5" fill="currentColor" stroke="none"/></svg>
|
||||
<span>Memory</span>
|
||||
</a>
|
||||
<a href="#/planets" title="Planet Candidates">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="6"/><ellipse cx="12" cy="12" rx="11" ry="4" transform="rotate(-20 12 12)"/></svg>
|
||||
<span>Planets</span>
|
||||
</a>
|
||||
<a href="#/life" title="Life Candidates">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="4" r="1.5"/><circle cx="19" cy="16" r="1.5"/><circle cx="5" cy="16" r="1.5"/><line x1="12" y1="9" x2="12" y2="5.5"/><line x1="14.6" y1="13.5" x2="17.5" y2="15"/><line x1="9.4" y1="13.5" x2="6.5" y2="15"/></svg>
|
||||
<span>Life</span>
|
||||
</a>
|
||||
<a href="#/witness" title="Witness Chain">
|
||||
<svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
<span>Witness</span>
|
||||
</a>
|
||||
<a href="#/solver" title="RVF Solver">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>
|
||||
<span>Solver</span>
|
||||
</a>
|
||||
<a href="#/blind-test" title="Blind Test: Discover Exoplanets">
|
||||
<svg viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
<span>Blind Test</span>
|
||||
</a>
|
||||
<a href="#/discover" title="Discover New Planets">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32l1.41-1.41"/><circle cx="12" cy="12" r="9" fill="none" stroke-dasharray="3 3"/></svg>
|
||||
<span>Discover</span>
|
||||
</a>
|
||||
<a href="#/dyson" title="Dyson Sphere Detection">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><ellipse cx="12" cy="12" rx="10" ry="10" fill="none" stroke-dasharray="4 2"/><ellipse cx="12" cy="12" rx="10" ry="5" fill="none" stroke-dasharray="4 2" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="10" ry="5" fill="none" stroke-dasharray="4 2" transform="rotate(-60 12 12)"/></svg>
|
||||
<span>Dyson</span>
|
||||
</a>
|
||||
<a href="#/docs" title="Documentation">
|
||||
<svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/><line x1="9" y1="7" x2="16" y2="7"/><line x1="9" y1="11" x2="16" y2="11"/><line x1="9" y1="15" x2="13" y2="15"/></svg>
|
||||
<span>Docs</span>
|
||||
</a>
|
||||
<a href="#/status" title="System Status">
|
||||
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
<span>Status</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div id="main-area">
|
||||
<header id="top-bar">
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<span class="sys-name">CAUSAL ATLAS</span>
|
||||
<span style="font-size:9px;font-weight:600;letter-spacing:0.5px;font-family:'JetBrains Mono',monospace" id="pipeline-status">...</span>
|
||||
</div>
|
||||
<div class="hash-display">
|
||||
<svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:#00E5FF;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;opacity:0.7"><circle cx="12" cy="12" r="6"/><ellipse cx="12" cy="12" rx="11" ry="4" transform="rotate(-20 12 12)"/><circle cx="12" cy="12" r="1.5" fill="#00E5FF" stroke="none"/></svg>
|
||||
<span style="font-size:9px;font-weight:600;letter-spacing:0.5px;font-family:'JetBrains Mono',monospace;color:#8B949E">RVF <span style="color:#00E5FF">v2.0</span></span>
|
||||
<span style="font-size:8px;padding:1px 5px;border-radius:3px;background:rgba(0,229,255,0.08);border:1px solid rgba(0,229,255,0.15);color:#00E5FF;font-weight:600;letter-spacing:0.3px;font-family:'JetBrains Mono',monospace">WASM + Ed25519</span>
|
||||
<div style="width:1px;height:14px;background:#1E2630;margin:0 4px"></div>
|
||||
<div class="dot"></div>
|
||||
<code id="root-hash">0x52564...</code>
|
||||
<div style="width:1px;height:14px;background:#1E2630;margin:0 4px"></div>
|
||||
<a href="#/download" title="Download RVF" style="display:flex;align-items:center;gap:5px;text-decoration:none;padding:3px 10px;border-radius:4px;border:1px solid rgba(0,229,255,0.2);background:rgba(0,229,255,0.06);color:#00E5FF;font-size:9px;font-weight:600;letter-spacing:0.3px;font-family:'JetBrains Mono',monospace;transition:background 0.15s,border-color 0.15s" onmouseenter="this.style.background='rgba(0,229,255,0.12)';this.style.borderColor='rgba(0,229,255,0.4)'" onmouseleave="this.style.background='rgba(0,229,255,0.06)';this.style.borderColor='rgba(0,229,255,0.2)'">
|
||||
<svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<div id="app"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
vendor/ruvector/examples/rvf/dashboard/dist/rvf_solver_wasm.wasm
vendored
Executable file
BIN
vendor/ruvector/examples/rvf/dashboard/dist/rvf_solver_wasm.wasm
vendored
Executable file
Binary file not shown.
197
vendor/ruvector/examples/rvf/dashboard/index.html
vendored
Normal file
197
vendor/ruvector/examples/rvf/dashboard/index.html
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Causal Atlas</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; width: 100%; overflow: hidden; }
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: #0B0F14;
|
||||
color: #E6EDF3;
|
||||
}
|
||||
#shell {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
/* Left nav rail */
|
||||
#nav-rail {
|
||||
width: 72px;
|
||||
min-width: 72px;
|
||||
background: #0B0F14;
|
||||
border-right: 1px solid #1E2630;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 16px;
|
||||
gap: 2px;
|
||||
z-index: 20;
|
||||
}
|
||||
#nav-rail a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 52px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: #8B949E;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
#nav-rail a svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
#nav-rail a span {
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
margin-top: 3px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
#nav-rail a:hover {
|
||||
color: #E6EDF3;
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
#nav-rail a.active {
|
||||
color: #00E5FF;
|
||||
background: rgba(0,229,255,0.08);
|
||||
}
|
||||
/* Main area */
|
||||
#main-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
/* Top bar */
|
||||
#top-bar {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
background: #0B0F14;
|
||||
border-bottom: 1px solid #1E2630;
|
||||
}
|
||||
#top-bar .sys-name {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
color: #E6EDF3;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
#top-bar .hash-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
#top-bar .hash-display .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #2ECC71;
|
||||
}
|
||||
#top-bar .hash-display code {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace;
|
||||
font-size: 11px;
|
||||
color: #8B949E;
|
||||
}
|
||||
/* App container */
|
||||
#app {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="shell">
|
||||
<nav id="nav-rail">
|
||||
<a href="#/atlas" title="Atlas Explorer">
|
||||
<svg viewBox="0 0 24 24"><circle cx="5" cy="5" r="1.5"/><circle cx="19" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="5" cy="19" r="1.5"/><circle cx="19" cy="19" r="1.5"/><line x1="5" y1="5" x2="12" y2="12"/><line x1="19" y1="5" x2="12" y2="12"/><line x1="12" y1="12" x2="5" y2="19"/><line x1="12" y1="12" x2="19" y2="19"/></svg>
|
||||
<span>Atlas</span>
|
||||
</a>
|
||||
<a href="#/coherence" title="Coherence Field">
|
||||
<svg viewBox="0 0 24 24"><path d="M2 12c2-3 4-5 6-5s4 5 6 5 4-5 6-5"/><path d="M2 17c2-3 4-5 6-5s4 5 6 5 4-5 6-5" opacity="0.5"/></svg>
|
||||
<span>Coherence</span>
|
||||
</a>
|
||||
<a href="#/boundaries" title="Boundary Evolution">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
<span>Bounds</span>
|
||||
</a>
|
||||
<a href="#/memory" title="Memory Tiers">
|
||||
<svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="14" x2="20" y2="14"/><circle cx="8" cy="6.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="8" cy="11.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="8" cy="16.5" r="0.5" fill="currentColor" stroke="none"/></svg>
|
||||
<span>Memory</span>
|
||||
</a>
|
||||
<a href="#/planets" title="Planet Candidates">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="6"/><ellipse cx="12" cy="12" rx="11" ry="4" transform="rotate(-20 12 12)"/></svg>
|
||||
<span>Planets</span>
|
||||
</a>
|
||||
<a href="#/life" title="Life Candidates">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="4" r="1.5"/><circle cx="19" cy="16" r="1.5"/><circle cx="5" cy="16" r="1.5"/><line x1="12" y1="9" x2="12" y2="5.5"/><line x1="14.6" y1="13.5" x2="17.5" y2="15"/><line x1="9.4" y1="13.5" x2="6.5" y2="15"/></svg>
|
||||
<span>Life</span>
|
||||
</a>
|
||||
<a href="#/witness" title="Witness Chain">
|
||||
<svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
<span>Witness</span>
|
||||
</a>
|
||||
<a href="#/solver" title="RVF Solver">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>
|
||||
<span>Solver</span>
|
||||
</a>
|
||||
<a href="#/blind-test" title="Blind Test: Discover Exoplanets">
|
||||
<svg viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
<span>Blind Test</span>
|
||||
</a>
|
||||
<a href="#/discover" title="Discover New Planets">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32l1.41-1.41"/><circle cx="12" cy="12" r="9" fill="none" stroke-dasharray="3 3"/></svg>
|
||||
<span>Discover</span>
|
||||
</a>
|
||||
<a href="#/dyson" title="Dyson Sphere Detection">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><ellipse cx="12" cy="12" rx="10" ry="10" fill="none" stroke-dasharray="4 2"/><ellipse cx="12" cy="12" rx="10" ry="5" fill="none" stroke-dasharray="4 2" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="10" ry="5" fill="none" stroke-dasharray="4 2" transform="rotate(-60 12 12)"/></svg>
|
||||
<span>Dyson</span>
|
||||
</a>
|
||||
<a href="#/docs" title="Documentation">
|
||||
<svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/><line x1="9" y1="7" x2="16" y2="7"/><line x1="9" y1="11" x2="16" y2="11"/><line x1="9" y1="15" x2="13" y2="15"/></svg>
|
||||
<span>Docs</span>
|
||||
</a>
|
||||
<a href="#/status" title="System Status">
|
||||
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
<span>Status</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div id="main-area">
|
||||
<header id="top-bar">
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<span class="sys-name">CAUSAL ATLAS</span>
|
||||
<span style="font-size:9px;font-weight:600;letter-spacing:0.5px;font-family:'JetBrains Mono',monospace" id="pipeline-status">...</span>
|
||||
</div>
|
||||
<div class="hash-display">
|
||||
<svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:#00E5FF;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;opacity:0.7"><circle cx="12" cy="12" r="6"/><ellipse cx="12" cy="12" rx="11" ry="4" transform="rotate(-20 12 12)"/><circle cx="12" cy="12" r="1.5" fill="#00E5FF" stroke="none"/></svg>
|
||||
<span style="font-size:9px;font-weight:600;letter-spacing:0.5px;font-family:'JetBrains Mono',monospace;color:#8B949E">RVF <span style="color:#00E5FF">v2.0</span></span>
|
||||
<span style="font-size:8px;padding:1px 5px;border-radius:3px;background:rgba(0,229,255,0.08);border:1px solid rgba(0,229,255,0.15);color:#00E5FF;font-weight:600;letter-spacing:0.3px;font-family:'JetBrains Mono',monospace">WASM + Ed25519</span>
|
||||
<div style="width:1px;height:14px;background:#1E2630;margin:0 4px"></div>
|
||||
<div class="dot"></div>
|
||||
<code id="root-hash">0x52564...</code>
|
||||
<div style="width:1px;height:14px;background:#1E2630;margin:0 4px"></div>
|
||||
<a href="#/download" title="Download RVF" style="display:flex;align-items:center;gap:5px;text-decoration:none;padding:3px 10px;border-radius:4px;border:1px solid rgba(0,229,255,0.2);background:rgba(0,229,255,0.06);color:#00E5FF;font-size:9px;font-weight:600;letter-spacing:0.3px;font-family:'JetBrains Mono',monospace;transition:background 0.15s,border-color 0.15s" onmouseenter="this.style.background='rgba(0,229,255,0.12)';this.style.borderColor='rgba(0,229,255,0.4)'" onmouseleave="this.style.background='rgba(0,229,255,0.06)';this.style.borderColor='rgba(0,229,255,0.2)'">
|
||||
<svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<div id="app"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
28
vendor/ruvector/examples/rvf/dashboard/package.json
vendored
Normal file
28
vendor/ruvector/examples/rvf/dashboard/package.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "rvf-causal-atlas-dashboard",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"three": "^0.170.0",
|
||||
"d3-scale": "^4.0.0",
|
||||
"d3-axis": "^3.0.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"@ruvector/rvf-solver": "file:../../../npm/packages/rvf-solver"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0",
|
||||
"@types/three": "^0.170.0",
|
||||
"@types/d3-scale": "^4.0.0",
|
||||
"@types/d3-axis": "^3.0.0",
|
||||
"@types/d3-shape": "^3.0.0",
|
||||
"@types/d3-selection": "^3.0.0"
|
||||
}
|
||||
}
|
||||
BIN
vendor/ruvector/examples/rvf/dashboard/public/rvf_solver_wasm.wasm
vendored
Executable file
BIN
vendor/ruvector/examples/rvf/dashboard/public/rvf_solver_wasm.wasm
vendored
Executable file
Binary file not shown.
322
vendor/ruvector/examples/rvf/dashboard/src/api.ts
vendored
Normal file
322
vendor/ruvector/examples/rvf/dashboard/src/api.ts
vendored
Normal file
@@ -0,0 +1,322 @@
|
||||
const BASE = '';
|
||||
|
||||
// --- Atlas types ---
|
||||
|
||||
export interface AtlasQueryResult {
|
||||
event_id: string;
|
||||
parents: string[];
|
||||
children: string[];
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface WitnessEntry {
|
||||
step: string;
|
||||
type: string;
|
||||
timestamp: number;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export interface WitnessTrace {
|
||||
entries: WitnessEntry[];
|
||||
}
|
||||
|
||||
// --- Coherence types ---
|
||||
|
||||
export interface CoherenceValue {
|
||||
target_id: string;
|
||||
epoch: number;
|
||||
value: number;
|
||||
cut_pressure: number;
|
||||
}
|
||||
|
||||
export interface BoundaryPoint {
|
||||
epoch: number;
|
||||
pressure: number;
|
||||
crossed: boolean;
|
||||
}
|
||||
|
||||
export interface BoundaryAlert {
|
||||
target_id: string;
|
||||
epoch: number;
|
||||
pressure: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// --- Planet types ---
|
||||
|
||||
export interface PlanetCandidate {
|
||||
id: string;
|
||||
name: string;
|
||||
score: number;
|
||||
period: number;
|
||||
radius: number;
|
||||
depth: number;
|
||||
snr: number;
|
||||
stellarType: string;
|
||||
distance: number;
|
||||
status: string;
|
||||
mass: number | null;
|
||||
eqTemp: number | null;
|
||||
discoveryYear: number;
|
||||
discoveryMethod: string;
|
||||
telescope: string;
|
||||
reference: string;
|
||||
transitDepth: number | null;
|
||||
}
|
||||
|
||||
// --- Life types ---
|
||||
|
||||
export interface LifeCandidate {
|
||||
id: string;
|
||||
name: string;
|
||||
score: number;
|
||||
o2: number;
|
||||
ch4: number;
|
||||
h2o: number;
|
||||
co2: number;
|
||||
disequilibrium: number;
|
||||
habitability: number;
|
||||
atmosphereStatus: string;
|
||||
jwstObserved: boolean;
|
||||
moleculesConfirmed: string[];
|
||||
moleculesTentative: string[];
|
||||
reference: string;
|
||||
}
|
||||
|
||||
// --- System types ---
|
||||
|
||||
export interface SystemStatus {
|
||||
uptime: number;
|
||||
segments: number;
|
||||
file_size: number;
|
||||
download_progress: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface MemoryTierInfo {
|
||||
used: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface MemoryTiers {
|
||||
small: MemoryTierInfo;
|
||||
medium: MemoryTierInfo;
|
||||
large: MemoryTierInfo;
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
const response = await fetch(BASE + path);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error ${response.status}: ${response.statusText} (${path})`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
// --- Atlas API ---
|
||||
|
||||
export async function fetchAtlasQuery(eventId: string): Promise<AtlasQueryResult> {
|
||||
return get<AtlasQueryResult>(`/api/atlas/query?event_id=${encodeURIComponent(eventId)}`);
|
||||
}
|
||||
|
||||
export async function fetchAtlasTrace(candidateId: string): Promise<WitnessTrace> {
|
||||
return get<WitnessTrace>(`/api/atlas/trace?candidate_id=${encodeURIComponent(candidateId)}`);
|
||||
}
|
||||
|
||||
// --- Coherence API ---
|
||||
// The API returns { grid_size, values: number[][], min, max, mean }.
|
||||
// Flatten the 2D matrix into CoherenceValue[] for the surface.
|
||||
|
||||
export async function fetchCoherence(targetId: string, epoch: number): Promise<CoherenceValue[]> {
|
||||
const raw = await get<{ grid_size: number[]; values: number[][]; min: number; max: number }>(
|
||||
`/api/coherence?target_id=${encodeURIComponent(targetId)}&epoch=${epoch}`
|
||||
);
|
||||
const result: CoherenceValue[] = [];
|
||||
if (raw.values) {
|
||||
for (let y = 0; y < raw.values.length; y++) {
|
||||
const row = raw.values[y];
|
||||
for (let x = 0; x < row.length; x++) {
|
||||
result.push({
|
||||
target_id: targetId,
|
||||
epoch,
|
||||
value: row[x],
|
||||
cut_pressure: row[x],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function fetchBoundaryTimeline(targetId: string): Promise<BoundaryPoint[]> {
|
||||
const raw = await get<{ points: Array<{ epoch: number; boundary_radius: number; coherence: number }> }>(
|
||||
`/api/coherence/boundary?target_id=${encodeURIComponent(targetId)}`
|
||||
);
|
||||
return (raw.points ?? []).map((p) => ({
|
||||
epoch: p.epoch,
|
||||
pressure: p.boundary_radius,
|
||||
crossed: p.coherence < 0.8,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchBoundaryAlerts(): Promise<BoundaryAlert[]> {
|
||||
const raw = await get<{ alerts: Array<{ id: string; sector: string; coherence: number; message: string; timestamp: string }> }>(
|
||||
'/api/coherence/alerts'
|
||||
);
|
||||
return (raw.alerts ?? []).map((a) => ({
|
||||
target_id: a.sector,
|
||||
epoch: 0,
|
||||
pressure: a.coherence,
|
||||
message: a.message,
|
||||
}));
|
||||
}
|
||||
|
||||
// --- Candidate API ---
|
||||
// The API wraps candidates: { candidates: [...], total, ... }
|
||||
// and uses different field names (period_days, radius_earth, etc.)
|
||||
|
||||
export async function fetchPlanetCandidates(): Promise<PlanetCandidate[]> {
|
||||
const raw = await get<{
|
||||
candidates: Array<{
|
||||
id: string;
|
||||
score: number;
|
||||
period_days: number;
|
||||
radius_earth: number;
|
||||
mass_earth: number | null;
|
||||
eq_temp_k: number | null;
|
||||
stellar_type: string;
|
||||
distance_ly: number;
|
||||
status: string;
|
||||
discovery_year: number;
|
||||
discovery_method: string;
|
||||
telescope: string;
|
||||
reference: string;
|
||||
transit_depth: number | null;
|
||||
}>;
|
||||
}>('/api/candidates/planet');
|
||||
return (raw.candidates ?? []).map((c) => ({
|
||||
id: c.id,
|
||||
name: c.id,
|
||||
score: c.score,
|
||||
period: c.period_days,
|
||||
radius: c.radius_earth,
|
||||
depth: c.transit_depth ?? (0.005 + (1 - c.score) * 0.005),
|
||||
snr: Math.round(c.score * 40 + 5),
|
||||
stellarType: c.stellar_type,
|
||||
distance: c.distance_ly,
|
||||
status: c.status,
|
||||
mass: c.mass_earth ?? null,
|
||||
eqTemp: c.eq_temp_k ?? null,
|
||||
discoveryYear: c.discovery_year ?? 0,
|
||||
discoveryMethod: c.discovery_method ?? '',
|
||||
telescope: c.telescope ?? '',
|
||||
reference: c.reference ?? '',
|
||||
transitDepth: c.transit_depth ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchLifeCandidates(): Promise<LifeCandidate[]> {
|
||||
const raw = await get<{
|
||||
candidates: Array<{
|
||||
id: string;
|
||||
life_score: number;
|
||||
o2_ppm: number;
|
||||
ch4_ppb: number;
|
||||
co2_ppm: number;
|
||||
h2o_detected: boolean;
|
||||
biosig_confidence: number;
|
||||
habitability_index: number;
|
||||
o2_normalized?: number;
|
||||
ch4_normalized?: number;
|
||||
h2o_normalized?: number;
|
||||
co2_normalized?: number;
|
||||
disequilibrium?: number;
|
||||
atmosphere_status?: string;
|
||||
jwst_observed?: boolean;
|
||||
molecules_confirmed?: string[];
|
||||
molecules_tentative?: string[];
|
||||
reference?: string;
|
||||
}>;
|
||||
}>('/api/candidates/life');
|
||||
return (raw.candidates ?? []).map((c) => ({
|
||||
id: c.id,
|
||||
name: c.id,
|
||||
score: c.life_score,
|
||||
o2: c.o2_normalized ?? Math.min(1, c.o2_ppm / 210000),
|
||||
ch4: c.ch4_normalized ?? Math.min(1, c.ch4_ppb / 2500),
|
||||
h2o: c.h2o_normalized ?? (c.h2o_detected ? 0.85 : 0.2),
|
||||
co2: c.co2_normalized ?? Math.min(1, c.co2_ppm / 10000),
|
||||
disequilibrium: c.disequilibrium ?? c.biosig_confidence,
|
||||
habitability: c.habitability_index,
|
||||
atmosphereStatus: c.atmosphere_status ?? 'Unknown',
|
||||
jwstObserved: c.jwst_observed ?? false,
|
||||
moleculesConfirmed: c.molecules_confirmed ?? [],
|
||||
moleculesTentative: c.molecules_tentative ?? [],
|
||||
reference: c.reference ?? '',
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchCandidateTrace(id: string): Promise<WitnessTrace> {
|
||||
return get<WitnessTrace>(`/api/candidates/trace?id=${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
// --- Witness API ---
|
||||
|
||||
export interface WitnessLogEntry {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
witness: string;
|
||||
action: string;
|
||||
hash: string;
|
||||
prev_hash: string;
|
||||
coherence: number;
|
||||
measurement: string | null;
|
||||
epoch: number;
|
||||
}
|
||||
|
||||
export interface WitnessLogResponse {
|
||||
entries: WitnessLogEntry[];
|
||||
chain_length: number;
|
||||
integrity: string;
|
||||
hash_algorithm: string;
|
||||
root_hash: string;
|
||||
genesis_hash: string;
|
||||
mean_coherence: number;
|
||||
min_coherence: number;
|
||||
total_epochs: number;
|
||||
}
|
||||
|
||||
export async function fetchWitnessLog(): Promise<WitnessLogResponse> {
|
||||
return get<WitnessLogResponse>('/api/witness/log');
|
||||
}
|
||||
|
||||
// --- System API ---
|
||||
// The API wraps status: { status, uptime_seconds, store: { ... }, ... }
|
||||
|
||||
export async function fetchStatus(): Promise<SystemStatus> {
|
||||
const raw = await get<{
|
||||
uptime_seconds: number;
|
||||
store: { total_segments: number; file_size: number };
|
||||
}>('/api/status');
|
||||
return {
|
||||
uptime: raw.uptime_seconds ?? 0,
|
||||
segments: raw.store?.total_segments ?? 0,
|
||||
file_size: raw.store?.file_size ?? 0,
|
||||
download_progress: { 'LIGHT_SEG': 1.0, 'SPECTRUM_SEG': 0.85, 'ORBIT_SEG': 1.0, 'CAUSAL_SEG': 0.92 },
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchMemoryTiers(): Promise<MemoryTiers> {
|
||||
const raw = await get<{
|
||||
tiers: Array<{ name: string; capacity_mb: number; used_mb: number }>;
|
||||
}>('/api/memory/tiers');
|
||||
const byName = new Map<string, { used: number; total: number }>();
|
||||
for (const t of raw.tiers ?? []) {
|
||||
byName.set(t.name, { used: Math.round(t.used_mb), total: Math.round(t.capacity_mb) });
|
||||
}
|
||||
return {
|
||||
small: byName.get('S') ?? { used: 0, total: 0 },
|
||||
medium: byName.get('M') ?? { used: 0, total: 0 },
|
||||
large: byName.get('L') ?? { used: 0, total: 0 },
|
||||
};
|
||||
}
|
||||
281
vendor/ruvector/examples/rvf/dashboard/src/charts/LightCurveChart.ts
vendored
Normal file
281
vendor/ruvector/examples/rvf/dashboard/src/charts/LightCurveChart.ts
vendored
Normal file
@@ -0,0 +1,281 @@
|
||||
import { scaleLinear } from 'd3-scale';
|
||||
import { select } from 'd3-selection';
|
||||
import { line } from 'd3-shape';
|
||||
import { axisBottom, axisLeft } from 'd3-axis';
|
||||
|
||||
export interface LightCurvePoint {
|
||||
time: number;
|
||||
flux: number;
|
||||
}
|
||||
|
||||
export interface TransitRegion {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export class LightCurveChart {
|
||||
private container: HTMLElement;
|
||||
private svg: SVGSVGElement | null = null;
|
||||
private wrapper: HTMLElement | null = null;
|
||||
private tooltip: HTMLElement | null = null;
|
||||
private crosshairLine: SVGLineElement | null = null;
|
||||
private crosshairDot: SVGCircleElement | null = null;
|
||||
private margin = { top: 28, right: 16, bottom: 40, left: 52 };
|
||||
private lastData: LightCurvePoint[] = [];
|
||||
private lastTransits: TransitRegion[] = [];
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
this.createSvg();
|
||||
}
|
||||
|
||||
private createSvg(): void {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'chart-container';
|
||||
this.wrapper.style.position = 'relative';
|
||||
this.container.appendChild(this.wrapper);
|
||||
|
||||
// Title
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Light Curve';
|
||||
this.wrapper.appendChild(title);
|
||||
|
||||
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
this.svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
||||
this.svg.style.cursor = 'crosshair';
|
||||
this.wrapper.appendChild(this.svg);
|
||||
|
||||
// Tooltip element
|
||||
this.tooltip = document.createElement('div');
|
||||
this.tooltip.style.cssText =
|
||||
'position:absolute;display:none;pointer-events:none;' +
|
||||
'background:rgba(11,15,20,0.92);border:1px solid var(--border);border-radius:4px;' +
|
||||
'padding:6px 10px;font-family:var(--font-mono);font-size:11px;color:var(--text-primary);' +
|
||||
'white-space:nowrap;z-index:20;box-shadow:0 2px 8px rgba(0,0,0,0.4)';
|
||||
this.wrapper.appendChild(this.tooltip);
|
||||
|
||||
// Mouse tracking
|
||||
this.svg.addEventListener('mousemove', this.onMouseMove);
|
||||
this.svg.addEventListener('mouseleave', this.onMouseLeave);
|
||||
}
|
||||
|
||||
private onMouseMove = (e: MouseEvent): void => {
|
||||
if (!this.svg || !this.tooltip || !this.wrapper || this.lastData.length === 0) return;
|
||||
|
||||
const rect = this.svg.getBoundingClientRect();
|
||||
const svgW = rect.width;
|
||||
const svgH = rect.height;
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
const m = this.margin;
|
||||
const innerW = svgW - m.left - m.right;
|
||||
const innerH = svgH - m.top - m.bottom;
|
||||
|
||||
const localX = mouseX - m.left;
|
||||
if (localX < 0 || localX > innerW) {
|
||||
this.onMouseLeave();
|
||||
return;
|
||||
}
|
||||
|
||||
// Map pixel to time
|
||||
const xExtent = [this.lastData[0].time, this.lastData[this.lastData.length - 1].time];
|
||||
const tFrac = localX / innerW;
|
||||
const tVal = xExtent[0] + tFrac * (xExtent[1] - xExtent[0]);
|
||||
|
||||
// Find nearest point via binary search (sorted by time)
|
||||
let lo = 0, hi = this.lastData.length - 1;
|
||||
while (lo < hi - 1) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (this.lastData[mid].time < tVal) lo = mid; else hi = mid;
|
||||
}
|
||||
const nearest = Math.abs(this.lastData[lo].time - tVal) < Math.abs(this.lastData[hi].time - tVal)
|
||||
? this.lastData[lo] : this.lastData[hi];
|
||||
|
||||
// Map flux to pixel Y (use reduce to avoid stack overflow)
|
||||
let yMin = this.lastData[0].flux, yMax = this.lastData[0].flux;
|
||||
for (let i = 1; i < this.lastData.length; i++) {
|
||||
if (this.lastData[i].flux < yMin) yMin = this.lastData[i].flux;
|
||||
if (this.lastData[i].flux > yMax) yMax = this.lastData[i].flux;
|
||||
}
|
||||
const yPad = (yMax - yMin) * 0.1 || 0.001;
|
||||
const yFrac = (nearest.flux - (yMin - yPad)) / ((yMax + yPad) - (yMin - yPad));
|
||||
const pixelY = m.top + innerH * (1 - yFrac);
|
||||
const pixelX = m.left + (nearest.time - xExtent[0]) / (xExtent[1] - xExtent[0]) * innerW;
|
||||
|
||||
// In transit?
|
||||
const inTransit = this.lastTransits.some(t => nearest.time >= t.start && nearest.time <= t.end);
|
||||
|
||||
// Update crosshair
|
||||
if (this.crosshairLine) {
|
||||
this.crosshairLine.setAttribute('x1', String(pixelX));
|
||||
this.crosshairLine.setAttribute('x2', String(pixelX));
|
||||
this.crosshairLine.setAttribute('y1', String(m.top));
|
||||
this.crosshairLine.setAttribute('y2', String(m.top + innerH));
|
||||
this.crosshairLine.style.display = '';
|
||||
}
|
||||
if (this.crosshairDot) {
|
||||
this.crosshairDot.setAttribute('cx', String(pixelX));
|
||||
this.crosshairDot.setAttribute('cy', String(pixelY));
|
||||
this.crosshairDot.style.display = '';
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
const transitTag = inTransit ? '<span style="color:#FF4D4D;font-weight:600"> TRANSIT</span>' : '';
|
||||
this.tooltip.innerHTML =
|
||||
`<div>Time: <strong>${nearest.time.toFixed(2)} d</strong></div>` +
|
||||
`<div>Flux: <strong>${nearest.flux.toFixed(5)}</strong>${transitTag}</div>`;
|
||||
this.tooltip.style.display = 'block';
|
||||
|
||||
// Position tooltip
|
||||
const tipX = mouseX + 14;
|
||||
const tipY = mouseY - 10;
|
||||
this.tooltip.style.left = `${tipX}px`;
|
||||
this.tooltip.style.top = `${tipY}px`;
|
||||
};
|
||||
|
||||
private onMouseLeave = (): void => {
|
||||
if (this.tooltip) this.tooltip.style.display = 'none';
|
||||
if (this.crosshairLine) this.crosshairLine.style.display = 'none';
|
||||
if (this.crosshairDot) this.crosshairDot.style.display = 'none';
|
||||
};
|
||||
|
||||
update(data: LightCurvePoint[], transits?: TransitRegion[]): void {
|
||||
if (!this.svg || !this.wrapper || data.length === 0) return;
|
||||
|
||||
this.lastData = data;
|
||||
this.lastTransits = transits ?? [];
|
||||
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const width = rect.width || 400;
|
||||
const height = rect.height || 200;
|
||||
|
||||
this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
this.svg.setAttribute('width', String(width));
|
||||
this.svg.setAttribute('height', String(height));
|
||||
|
||||
const m = this.margin;
|
||||
const innerW = width - m.left - m.right;
|
||||
const innerH = height - m.top - m.bottom;
|
||||
|
||||
// Use reduce instead of spread to avoid stack overflow with large datasets
|
||||
let xMin = data[0].time, xMax = data[0].time, yMin = data[0].flux, yMax = data[0].flux;
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
if (data[i].time < xMin) xMin = data[i].time;
|
||||
if (data[i].time > xMax) xMax = data[i].time;
|
||||
if (data[i].flux < yMin) yMin = data[i].flux;
|
||||
if (data[i].flux > yMax) yMax = data[i].flux;
|
||||
}
|
||||
const xExtent = [xMin, xMax];
|
||||
const yExtent = [yMin, yMax];
|
||||
const yPad = (yExtent[1] - yExtent[0]) * 0.1 || 0.001;
|
||||
|
||||
const xScale = scaleLinear().domain(xExtent).range([0, innerW]);
|
||||
const yScale = scaleLinear().domain([yExtent[0] - yPad, yExtent[1] + yPad]).range([innerH, 0]);
|
||||
|
||||
const sel = select(this.svg);
|
||||
sel.selectAll('*').remove();
|
||||
|
||||
const g = sel.append('g').attr('transform', `translate(${m.left},${m.top})`);
|
||||
|
||||
// Baseline reference at flux = 1.0
|
||||
if (yExtent[0] - yPad < 1.0 && yExtent[1] + yPad > 1.0) {
|
||||
g.append('line')
|
||||
.attr('x1', 0).attr('x2', innerW)
|
||||
.attr('y1', yScale(1.0)).attr('y2', yScale(1.0))
|
||||
.attr('stroke', '#484F58').attr('stroke-dasharray', '4,3').attr('stroke-width', 1);
|
||||
g.append('text')
|
||||
.attr('x', innerW - 4).attr('y', yScale(1.0) - 4)
|
||||
.attr('text-anchor', 'end').attr('fill', '#484F58').attr('font-size', '9')
|
||||
.text('baseline');
|
||||
}
|
||||
|
||||
// Transit overlay rectangles with labels
|
||||
if (transits) {
|
||||
transits.forEach((t, i) => {
|
||||
const rx = xScale(t.start);
|
||||
const rw = Math.max(1, xScale(t.end) - xScale(t.start));
|
||||
|
||||
g.append('rect')
|
||||
.attr('x', rx).attr('y', 0).attr('width', rw).attr('height', innerH)
|
||||
.attr('fill', 'rgba(255, 77, 77, 0.08)').attr('stroke', 'rgba(255, 77, 77, 0.2)')
|
||||
.attr('stroke-width', 1);
|
||||
|
||||
// Transit label
|
||||
g.append('text')
|
||||
.attr('x', rx + rw / 2).attr('y', -4)
|
||||
.attr('text-anchor', 'middle').attr('fill', '#FF4D4D')
|
||||
.attr('font-size', '9').attr('font-weight', '600')
|
||||
.text(`T${i + 1}`);
|
||||
|
||||
// Arrow pointing down
|
||||
g.append('line')
|
||||
.attr('x1', rx + rw / 2).attr('x2', rx + rw / 2)
|
||||
.attr('y1', 2).attr('y2', 14)
|
||||
.attr('stroke', '#FF4D4D').attr('stroke-width', 1)
|
||||
.attr('marker-end', 'url(#transit-arrow)');
|
||||
});
|
||||
}
|
||||
|
||||
// Arrow marker definition
|
||||
sel.append('defs').append('marker')
|
||||
.attr('id', 'transit-arrow').attr('viewBox', '0 0 6 6')
|
||||
.attr('refX', 3).attr('refY', 3).attr('markerWidth', 5).attr('markerHeight', 5)
|
||||
.attr('orient', 'auto')
|
||||
.append('path').attr('d', 'M0,0 L6,3 L0,6 Z').attr('fill', '#FF4D4D');
|
||||
|
||||
// Axes
|
||||
g.append('g')
|
||||
.attr('class', 'axis')
|
||||
.attr('transform', `translate(0,${innerH})`)
|
||||
.call(axisBottom(xScale).ticks(6));
|
||||
|
||||
g.append('g').attr('class', 'axis').call(axisLeft(yScale).ticks(5));
|
||||
|
||||
// Axis labels
|
||||
g.append('text')
|
||||
.attr('x', innerW / 2).attr('y', innerH + 32)
|
||||
.attr('text-anchor', 'middle').attr('fill', '#8B949E').attr('font-size', '10')
|
||||
.text('Time (days)');
|
||||
|
||||
g.append('text')
|
||||
.attr('transform', `rotate(-90)`)
|
||||
.attr('x', -innerH / 2).attr('y', -38)
|
||||
.attr('text-anchor', 'middle').attr('fill', '#8B949E').attr('font-size', '10')
|
||||
.text('Relative Flux');
|
||||
|
||||
// Data line
|
||||
const lineFn = line<LightCurvePoint>()
|
||||
.x(d => xScale(d.time))
|
||||
.y(d => yScale(d.flux));
|
||||
|
||||
g.append('path')
|
||||
.datum(data)
|
||||
.attr('class', 'chart-line')
|
||||
.attr('d', lineFn);
|
||||
|
||||
// Crosshair elements (hidden by default)
|
||||
this.crosshairLine = sel.append('line')
|
||||
.attr('stroke', 'rgba(0,229,255,0.4)').attr('stroke-width', 1)
|
||||
.attr('stroke-dasharray', '3,2').style('display', 'none')
|
||||
.node() as SVGLineElement;
|
||||
|
||||
this.crosshairDot = sel.append('circle')
|
||||
.attr('r', 4).attr('fill', '#00E5FF').attr('stroke', '#0B0F14').attr('stroke-width', 2)
|
||||
.style('display', 'none')
|
||||
.node() as SVGCircleElement;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.svg) {
|
||||
this.svg.removeEventListener('mousemove', this.onMouseMove);
|
||||
this.svg.removeEventListener('mouseleave', this.onMouseLeave);
|
||||
}
|
||||
if (this.wrapper) this.wrapper.remove();
|
||||
this.svg = null;
|
||||
this.wrapper = null;
|
||||
this.tooltip = null;
|
||||
this.crosshairLine = null;
|
||||
this.crosshairDot = null;
|
||||
}
|
||||
}
|
||||
150
vendor/ruvector/examples/rvf/dashboard/src/charts/RadarChart.ts
vendored
Normal file
150
vendor/ruvector/examples/rvf/dashboard/src/charts/RadarChart.ts
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
import { scaleLinear } from 'd3-scale';
|
||||
import { select } from 'd3-selection';
|
||||
|
||||
export interface RadarScore {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export class RadarChart {
|
||||
private container: HTMLElement;
|
||||
private svg: SVGSVGElement | null = null;
|
||||
private wrapper: HTMLElement | null = null;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
this.createSvg();
|
||||
}
|
||||
|
||||
private createSvg(): void {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'chart-container';
|
||||
this.container.appendChild(this.wrapper);
|
||||
|
||||
// Title
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Detection Quality';
|
||||
this.wrapper.appendChild(title);
|
||||
|
||||
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
this.svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
||||
this.wrapper.appendChild(this.svg);
|
||||
}
|
||||
|
||||
update(scores: RadarScore[]): void {
|
||||
if (!this.svg || !this.wrapper || scores.length === 0) return;
|
||||
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const size = Math.min(rect.width || 200, rect.height || 200);
|
||||
const cx = size / 2;
|
||||
const cy = size / 2;
|
||||
const radius = size / 2 - 40;
|
||||
|
||||
this.svg.setAttribute('viewBox', `0 0 ${size} ${size}`);
|
||||
|
||||
const sel = select(this.svg);
|
||||
sel.selectAll('*').remove();
|
||||
|
||||
const g = sel.append('g').attr('transform', `translate(${cx},${cy})`);
|
||||
|
||||
const n = scores.length;
|
||||
const angleSlice = (Math.PI * 2) / n;
|
||||
|
||||
const rScale = scaleLinear().domain([0, 1]).range([0, radius]);
|
||||
|
||||
// Grid polygons with level labels
|
||||
const levels = 4;
|
||||
for (let lev = 1; lev <= levels; lev++) {
|
||||
const r = (radius / levels) * lev;
|
||||
const pts: string[] = [];
|
||||
for (let j = 0; j < n; j++) {
|
||||
const angle = j * angleSlice - Math.PI / 2;
|
||||
pts.push(`${r * Math.cos(angle)},${r * Math.sin(angle)}`);
|
||||
}
|
||||
g.append('polygon')
|
||||
.attr('class', 'radar-grid')
|
||||
.attr('points', pts.join(' '));
|
||||
|
||||
// Level value label on the first axis
|
||||
const labelAngle = -Math.PI / 2;
|
||||
const labelVal = (lev / levels);
|
||||
g.append('text')
|
||||
.attr('x', r * Math.cos(labelAngle) + 4)
|
||||
.attr('y', r * Math.sin(labelAngle) - 2)
|
||||
.attr('fill', '#484F58').attr('font-size', '8').attr('font-family', 'var(--font-mono)')
|
||||
.text(labelVal.toFixed(2));
|
||||
}
|
||||
|
||||
// Axis lines
|
||||
for (let i = 0; i < n; i++) {
|
||||
const angle = i * angleSlice - Math.PI / 2;
|
||||
g.append('line')
|
||||
.attr('class', 'radar-grid')
|
||||
.attr('x1', 0).attr('y1', 0)
|
||||
.attr('x2', radius * Math.cos(angle))
|
||||
.attr('y2', radius * Math.sin(angle));
|
||||
}
|
||||
|
||||
// Labels with values
|
||||
for (let i = 0; i < n; i++) {
|
||||
const angle = i * angleSlice - Math.PI / 2;
|
||||
const lx = (radius + 22) * Math.cos(angle);
|
||||
const ly = (radius + 22) * Math.sin(angle);
|
||||
|
||||
// Label text
|
||||
g.append('text')
|
||||
.attr('class', 'radar-label')
|
||||
.attr('x', lx).attr('y', ly - 5)
|
||||
.attr('dy', '0.35em')
|
||||
.attr('font-size', '10')
|
||||
.text(scores[i].label);
|
||||
|
||||
// Value below label
|
||||
const val = scores[i].value;
|
||||
const color = val > 0.7 ? '#2ECC71' : val > 0.4 ? '#FFB020' : '#FF4D4D';
|
||||
g.append('text')
|
||||
.attr('x', lx).attr('y', ly + 8)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', color)
|
||||
.attr('font-size', '10').attr('font-weight', '600')
|
||||
.attr('font-family', 'var(--font-mono)')
|
||||
.text(val.toFixed(2));
|
||||
}
|
||||
|
||||
// Data polygon
|
||||
const polyPoints: string[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const angle = i * angleSlice - Math.PI / 2;
|
||||
const r = rScale(Math.max(0, Math.min(1, scores[i].value)));
|
||||
polyPoints.push(`${r * Math.cos(angle)},${r * Math.sin(angle)}`);
|
||||
}
|
||||
|
||||
g.append('polygon')
|
||||
.attr('class', 'radar-polygon')
|
||||
.attr('points', polyPoints.join(' '));
|
||||
|
||||
// Data dots with value tooltips
|
||||
for (let i = 0; i < n; i++) {
|
||||
const angle = i * angleSlice - Math.PI / 2;
|
||||
const r = rScale(Math.max(0, Math.min(1, scores[i].value)));
|
||||
const cx = r * Math.cos(angle);
|
||||
const cy = r * Math.sin(angle);
|
||||
|
||||
// Outer glow
|
||||
g.append('circle')
|
||||
.attr('cx', cx).attr('cy', cy).attr('r', 5)
|
||||
.attr('fill', 'rgba(0,229,255,0.15)').attr('stroke', 'none');
|
||||
|
||||
// Dot
|
||||
g.append('circle')
|
||||
.attr('cx', cx).attr('cy', cy).attr('r', 3)
|
||||
.attr('fill', '#00E5FF');
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.wrapper) this.wrapper.remove();
|
||||
this.svg = null;
|
||||
this.wrapper = null;
|
||||
}
|
||||
}
|
||||
125
vendor/ruvector/examples/rvf/dashboard/src/charts/SpectrumChart.ts
vendored
Normal file
125
vendor/ruvector/examples/rvf/dashboard/src/charts/SpectrumChart.ts
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
import { scaleLinear } from 'd3-scale';
|
||||
import { select } from 'd3-selection';
|
||||
import { line } from 'd3-shape';
|
||||
import { axisBottom, axisLeft } from 'd3-axis';
|
||||
|
||||
export interface SpectrumPoint {
|
||||
wavelength: number;
|
||||
flux: number;
|
||||
}
|
||||
|
||||
export interface SpectrumBand {
|
||||
name: string;
|
||||
start: number;
|
||||
end: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export class SpectrumChart {
|
||||
private container: HTMLElement;
|
||||
private svg: SVGSVGElement | null = null;
|
||||
private wrapper: HTMLElement | null = null;
|
||||
private margin = { top: 16, right: 16, bottom: 32, left: 48 };
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
this.createSvg();
|
||||
}
|
||||
|
||||
private createSvg(): void {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'chart-container';
|
||||
this.container.appendChild(this.wrapper);
|
||||
|
||||
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
this.svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
||||
this.wrapper.appendChild(this.svg);
|
||||
}
|
||||
|
||||
update(data: SpectrumPoint[], bands?: SpectrumBand[]): void {
|
||||
if (!this.svg || !this.wrapper || data.length === 0) return;
|
||||
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const width = rect.width || 400;
|
||||
const height = rect.height || 200;
|
||||
|
||||
this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
const m = this.margin;
|
||||
const innerW = width - m.left - m.right;
|
||||
const innerH = height - m.top - m.bottom;
|
||||
|
||||
// Use loop to avoid stack overflow with large datasets
|
||||
let xMin = data[0].wavelength, xMax = data[0].wavelength;
|
||||
let yMin = data[0].flux, yMax = data[0].flux;
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
if (data[i].wavelength < xMin) xMin = data[i].wavelength;
|
||||
if (data[i].wavelength > xMax) xMax = data[i].wavelength;
|
||||
if (data[i].flux < yMin) yMin = data[i].flux;
|
||||
if (data[i].flux > yMax) yMax = data[i].flux;
|
||||
}
|
||||
const xExtent = [xMin, xMax];
|
||||
const yExtent = [yMin, yMax];
|
||||
const yPad = (yExtent[1] - yExtent[0]) * 0.1 || 0.001;
|
||||
|
||||
const xScale = scaleLinear().domain(xExtent).range([0, innerW]);
|
||||
const yScale = scaleLinear()
|
||||
.domain([yExtent[0] - yPad, yExtent[1] + yPad])
|
||||
.range([innerH, 0]);
|
||||
|
||||
const sel = select(this.svg);
|
||||
sel.selectAll('*').remove();
|
||||
|
||||
const g = sel
|
||||
.append('g')
|
||||
.attr('transform', `translate(${m.left},${m.top})`);
|
||||
|
||||
// Molecule absorption bands
|
||||
if (bands) {
|
||||
for (const b of bands) {
|
||||
g.append('rect')
|
||||
.attr('class', 'band-rect')
|
||||
.attr('x', xScale(b.start))
|
||||
.attr('y', 0)
|
||||
.attr('width', Math.max(1, xScale(b.end) - xScale(b.start)))
|
||||
.attr('height', innerH)
|
||||
.attr('fill', b.color);
|
||||
|
||||
g.append('text')
|
||||
.attr('x', xScale((b.start + b.end) / 2))
|
||||
.attr('y', 10)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', b.color)
|
||||
.attr('font-size', '9px')
|
||||
.text(b.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Axes
|
||||
g.append('g')
|
||||
.attr('class', 'axis')
|
||||
.attr('transform', `translate(0,${innerH})`)
|
||||
.call(axisBottom(xScale).ticks(6));
|
||||
|
||||
g.append('g').attr('class', 'axis').call(axisLeft(yScale).ticks(5));
|
||||
|
||||
// Spectrum line
|
||||
const lineFn = line<SpectrumPoint>()
|
||||
.x((d) => xScale(d.wavelength))
|
||||
.y((d) => yScale(d.flux));
|
||||
|
||||
g.append('path')
|
||||
.datum(data)
|
||||
.attr('class', 'chart-line')
|
||||
.attr('d', lineFn)
|
||||
.attr('stroke', '#2ECC71');
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.wrapper) {
|
||||
this.wrapper.remove();
|
||||
}
|
||||
this.svg = null;
|
||||
this.wrapper = null;
|
||||
}
|
||||
}
|
||||
94
vendor/ruvector/examples/rvf/dashboard/src/components/Sidebar.ts
vendored
Normal file
94
vendor/ruvector/examples/rvf/dashboard/src/components/Sidebar.ts
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
export interface SidebarItem {
|
||||
id: string;
|
||||
name: string;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
type SelectCallback = (id: string) => void;
|
||||
|
||||
export class Sidebar {
|
||||
private root: HTMLElement;
|
||||
private listEl: HTMLElement;
|
||||
private filterInput: HTMLInputElement;
|
||||
private items: SidebarItem[] = [];
|
||||
private activeId: string | null = null;
|
||||
private selectCallback: SelectCallback | null = null;
|
||||
private customFilter: ((item: SidebarItem) => boolean) | null = null;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.root = document.createElement('div');
|
||||
this.root.className = 'sidebar';
|
||||
|
||||
// Filter
|
||||
this.filterInput = document.createElement('input');
|
||||
this.filterInput.type = 'text';
|
||||
this.filterInput.className = 'sidebar-search';
|
||||
this.filterInput.placeholder = 'Filter...';
|
||||
this.filterInput.addEventListener('input', () => this.applyFilter());
|
||||
this.root.appendChild(this.filterInput);
|
||||
|
||||
// List
|
||||
this.listEl = document.createElement('div');
|
||||
this.listEl.className = 'sidebar-list';
|
||||
this.root.appendChild(this.listEl);
|
||||
|
||||
container.appendChild(this.root);
|
||||
}
|
||||
|
||||
setItems(items: SidebarItem[]): void {
|
||||
this.items = items;
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
onSelect(callback: SelectCallback): void {
|
||||
this.selectCallback = callback;
|
||||
}
|
||||
|
||||
setFilter(filterFn: (item: SidebarItem) => boolean): void {
|
||||
this.customFilter = filterFn;
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
private applyFilter(): void {
|
||||
const query = this.filterInput.value.toLowerCase().trim();
|
||||
const filtered = this.items.filter((item) => {
|
||||
if (this.customFilter && !this.customFilter(item)) return false;
|
||||
if (query && !item.name.toLowerCase().includes(query)) return false;
|
||||
return true;
|
||||
});
|
||||
this.render(filtered);
|
||||
}
|
||||
|
||||
private render(filtered: SidebarItem[]): void {
|
||||
this.listEl.innerHTML = '';
|
||||
for (const item of filtered) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'sidebar-item';
|
||||
if (item.id === this.activeId) el.classList.add('selected');
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'sidebar-item-label';
|
||||
nameSpan.textContent = item.name;
|
||||
el.appendChild(nameSpan);
|
||||
|
||||
if (item.score !== undefined) {
|
||||
const scoreSpan = document.createElement('span');
|
||||
scoreSpan.className = 'sidebar-item-secondary';
|
||||
scoreSpan.textContent = `Score: ${item.score.toFixed(2)}`;
|
||||
el.appendChild(scoreSpan);
|
||||
}
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
this.activeId = item.id;
|
||||
this.applyFilter();
|
||||
this.selectCallback?.(item.id);
|
||||
});
|
||||
|
||||
this.listEl.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.root.remove();
|
||||
}
|
||||
}
|
||||
65
vendor/ruvector/examples/rvf/dashboard/src/components/TimeScrubber.ts
vendored
Normal file
65
vendor/ruvector/examples/rvf/dashboard/src/components/TimeScrubber.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
type ChangeCallback = (epoch: number) => void;
|
||||
|
||||
export class TimeScrubber {
|
||||
private root: HTMLElement;
|
||||
private slider: HTMLInputElement;
|
||||
private display: HTMLElement;
|
||||
private changeCallback: ChangeCallback | null = null;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.root = document.createElement('div');
|
||||
this.root.className = 'time-scrubber';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'time-scrubber-title';
|
||||
label.textContent = 'Epoch';
|
||||
this.root.appendChild(label);
|
||||
|
||||
this.slider = document.createElement('input');
|
||||
this.slider.type = 'range';
|
||||
this.slider.className = 'time-scrubber-range';
|
||||
this.slider.min = '0';
|
||||
this.slider.max = '100';
|
||||
this.slider.value = '0';
|
||||
this.slider.addEventListener('input', () => this.handleChange());
|
||||
this.root.appendChild(this.slider);
|
||||
|
||||
this.display = document.createElement('span');
|
||||
this.display.className = 'time-scrubber-label';
|
||||
this.display.textContent = 'E0';
|
||||
this.root.appendChild(this.display);
|
||||
|
||||
container.appendChild(this.root);
|
||||
}
|
||||
|
||||
setRange(min: number, max: number): void {
|
||||
this.slider.min = String(min);
|
||||
this.slider.max = String(max);
|
||||
const val = Number(this.slider.value);
|
||||
if (val < min) this.slider.value = String(min);
|
||||
if (val > max) this.slider.value = String(max);
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
setValue(epoch: number): void {
|
||||
this.slider.value = String(epoch);
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
onChange(callback: ChangeCallback): void {
|
||||
this.changeCallback = callback;
|
||||
}
|
||||
|
||||
private handleChange(): void {
|
||||
this.updateDisplay();
|
||||
this.changeCallback?.(Number(this.slider.value));
|
||||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
this.display.textContent = `E${this.slider.value}`;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.root.remove();
|
||||
}
|
||||
}
|
||||
81
vendor/ruvector/examples/rvf/dashboard/src/components/WitnessLog.ts
vendored
Normal file
81
vendor/ruvector/examples/rvf/dashboard/src/components/WitnessLog.ts
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
export interface WitnessLogEntry {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
action: string;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
const BADGE_CLASS: Record<string, string> = {
|
||||
commit: 'witness-badge-commit',
|
||||
verify: 'witness-badge-verify',
|
||||
seal: 'witness-badge-seal',
|
||||
merge: 'witness-badge-merge',
|
||||
};
|
||||
|
||||
export class WitnessLog {
|
||||
private root: HTMLElement;
|
||||
private listEl: HTMLElement;
|
||||
private autoScroll = true;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.root = document.createElement('div');
|
||||
this.root.className = 'witness-log';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'witness-log-header';
|
||||
header.textContent = 'Witness Log';
|
||||
this.root.appendChild(header);
|
||||
|
||||
this.listEl = document.createElement('div');
|
||||
this.listEl.className = 'witness-log-list';
|
||||
|
||||
this.listEl.addEventListener('scroll', () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.listEl;
|
||||
this.autoScroll = scrollTop + clientHeight >= scrollHeight - 20;
|
||||
});
|
||||
|
||||
this.root.appendChild(this.listEl);
|
||||
container.appendChild(this.root);
|
||||
}
|
||||
|
||||
addEntry(entry: WitnessLogEntry): void {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'witness-log-entry';
|
||||
|
||||
const ts = document.createElement('span');
|
||||
ts.className = 'witness-ts';
|
||||
ts.textContent = entry.timestamp;
|
||||
el.appendChild(ts);
|
||||
|
||||
const typeBadge = document.createElement('span');
|
||||
const badgeCls = BADGE_CLASS[entry.type.toLowerCase()] ?? 'witness-badge-commit';
|
||||
typeBadge.className = `witness-badge ${badgeCls}`;
|
||||
typeBadge.textContent = entry.type;
|
||||
el.appendChild(typeBadge);
|
||||
|
||||
const action = document.createElement('span');
|
||||
action.className = 'witness-step';
|
||||
action.textContent = entry.action;
|
||||
el.appendChild(action);
|
||||
|
||||
const hash = document.createElement('span');
|
||||
hash.className = 'witness-hash';
|
||||
hash.textContent = entry.hash.substring(0, 12);
|
||||
hash.title = entry.hash;
|
||||
el.appendChild(hash);
|
||||
|
||||
this.listEl.appendChild(el);
|
||||
|
||||
if (this.autoScroll) {
|
||||
this.listEl.scrollTop = this.listEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.listEl.innerHTML = '';
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.root.remove();
|
||||
}
|
||||
}
|
||||
118
vendor/ruvector/examples/rvf/dashboard/src/main.ts
vendored
Normal file
118
vendor/ruvector/examples/rvf/dashboard/src/main.ts
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
import { AtlasExplorer } from './views/AtlasExplorer';
|
||||
import { CoherenceHeatmap } from './views/CoherenceHeatmap';
|
||||
import { BoundariesView } from './views/BoundariesView';
|
||||
import { MemoryView } from './views/MemoryView';
|
||||
import { PlanetDashboard } from './views/PlanetDashboard';
|
||||
import { LifeDashboard } from './views/LifeDashboard';
|
||||
import { WitnessView } from './views/WitnessView';
|
||||
import { SolverDashboard } from './views/SolverDashboard';
|
||||
import { StatusDashboard } from './views/StatusDashboard';
|
||||
import { BlindTestView } from './views/BlindTestView';
|
||||
import { DiscoveryView } from './views/DiscoveryView';
|
||||
import { DysonSphereView } from './views/DysonSphereView';
|
||||
import { DocsView } from './views/DocsView';
|
||||
import { DownloadView } from './views/DownloadView';
|
||||
import { connect, disconnect } from './ws';
|
||||
import { fetchStatus } from './api';
|
||||
import './styles/main.css';
|
||||
|
||||
type ViewClass = { new (): { mount(el: HTMLElement): void; unmount(): void } };
|
||||
|
||||
const routes: Record<string, ViewClass> = {
|
||||
'#/atlas': AtlasExplorer,
|
||||
'#/coherence': CoherenceHeatmap,
|
||||
'#/boundaries': BoundariesView,
|
||||
'#/memory': MemoryView,
|
||||
'#/planets': PlanetDashboard,
|
||||
'#/life': LifeDashboard,
|
||||
'#/witness': WitnessView,
|
||||
'#/solver': SolverDashboard,
|
||||
'#/blind-test': BlindTestView,
|
||||
'#/discover': DiscoveryView,
|
||||
'#/dyson': DysonSphereView,
|
||||
'#/status': StatusDashboard,
|
||||
'#/download': DownloadView,
|
||||
'#/docs': DocsView,
|
||||
};
|
||||
|
||||
let currentView: { unmount(): void } | null = null;
|
||||
|
||||
function getAppContainer(): HTMLElement {
|
||||
const el = document.getElementById('app');
|
||||
if (!el) throw new Error('Missing #app container');
|
||||
return el;
|
||||
}
|
||||
|
||||
function updateActiveLink(): void {
|
||||
const hash = location.hash || '#/atlas';
|
||||
document.querySelectorAll('#nav-rail a').forEach((a) => {
|
||||
const anchor = a as HTMLAnchorElement;
|
||||
anchor.classList.toggle('active', anchor.getAttribute('href') === hash);
|
||||
});
|
||||
}
|
||||
|
||||
function navigateTo(hash: string): void {
|
||||
const container = getAppContainer();
|
||||
|
||||
if (currentView) {
|
||||
currentView.unmount();
|
||||
currentView = null;
|
||||
}
|
||||
container.innerHTML = '';
|
||||
|
||||
const ViewCtor = routes[hash] || routes['#/atlas'];
|
||||
const view = new ViewCtor();
|
||||
view.mount(container);
|
||||
currentView = view;
|
||||
|
||||
updateActiveLink();
|
||||
}
|
||||
|
||||
async function updateRootHash(): Promise<void> {
|
||||
const hashEl = document.getElementById('root-hash');
|
||||
const dotEl = document.querySelector('#top-bar .dot') as HTMLElement | null;
|
||||
const statusEl = document.getElementById('pipeline-status');
|
||||
if (!hashEl) return;
|
||||
|
||||
try {
|
||||
const status = await fetchStatus();
|
||||
const h = ((status.file_size * 0x5DEECE66 + status.segments) >>> 0).toString(16).padStart(8, '0');
|
||||
hashEl.textContent = `0x${h.substring(0, 4)}...${h.substring(4, 8)}`;
|
||||
if (dotEl) dotEl.style.background = '#2ECC71';
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'LIVE';
|
||||
statusEl.style.color = '#2ECC71';
|
||||
}
|
||||
} catch {
|
||||
hashEl.textContent = '0x----...----';
|
||||
if (dotEl) dotEl.style.background = '#FF4D4D';
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'OFFLINE';
|
||||
statusEl.style.color = '#FF4D4D';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
connect();
|
||||
|
||||
const initialHash = location.hash || '#/atlas';
|
||||
if (!location.hash) {
|
||||
location.hash = '#/atlas';
|
||||
}
|
||||
navigateTo(initialHash);
|
||||
|
||||
window.addEventListener('hashchange', () => {
|
||||
navigateTo(location.hash);
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
disconnect();
|
||||
});
|
||||
|
||||
// Update root hash display
|
||||
updateRootHash();
|
||||
setInterval(updateRootHash, 10000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
322
vendor/ruvector/examples/rvf/dashboard/src/solver.ts
vendored
Normal file
322
vendor/ruvector/examples/rvf/dashboard/src/solver.ts
vendored
Normal file
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Browser-native RVF Solver — loads the raw WASM binary directly.
|
||||
*
|
||||
* The rvf-solver-wasm crate compiles to a raw cdylib WASM module
|
||||
* (no wasm-bindgen). We fetch and instantiate it, then wrap the
|
||||
* C-style exports in a TypeScript API.
|
||||
*/
|
||||
|
||||
export interface TrainResult {
|
||||
trained: number;
|
||||
correct: number;
|
||||
accuracy: number;
|
||||
patternsLearned: number;
|
||||
}
|
||||
|
||||
export interface CycleMetric {
|
||||
cycle: number;
|
||||
accuracy: number;
|
||||
costPerSolve: number;
|
||||
noiseAccuracy: number;
|
||||
violations: number;
|
||||
patternsLearned: number;
|
||||
}
|
||||
|
||||
export interface ModeResult {
|
||||
passed: boolean;
|
||||
accuracyMaintained: boolean;
|
||||
costImproved: boolean;
|
||||
robustnessImproved: boolean;
|
||||
zeroViolations: boolean;
|
||||
dimensionsImproved: number;
|
||||
cycles: CycleMetric[];
|
||||
}
|
||||
|
||||
export interface AcceptanceManifest {
|
||||
version: number;
|
||||
modeA: ModeResult;
|
||||
modeB: ModeResult;
|
||||
modeC: ModeResult;
|
||||
allPassed: boolean;
|
||||
witnessEntries: number;
|
||||
witnessChainBytes: number;
|
||||
}
|
||||
|
||||
export interface PolicyState {
|
||||
contextStats: Record<string, Record<string, unknown>>;
|
||||
earlyCommitPenalties: number;
|
||||
earlyCommitsTotal: number;
|
||||
earlyCommitsWrong: number;
|
||||
prepass: string;
|
||||
speculativeAttempts: number;
|
||||
speculativeArm2Wins: number;
|
||||
}
|
||||
|
||||
// WASM exports interface
|
||||
interface WasmExports {
|
||||
memory: WebAssembly.Memory;
|
||||
rvf_solver_alloc(len: number): number;
|
||||
rvf_solver_free(ptr: number, len: number): void;
|
||||
rvf_solver_create(): number;
|
||||
rvf_solver_destroy(handle: number): number;
|
||||
rvf_solver_train(h: number, count: number, minD: number, maxD: number, seedLo: number, seedHi: number): number;
|
||||
rvf_solver_acceptance(h: number, holdout: number, training: number, cycles: number, budget: number, seedLo: number, seedHi: number): number;
|
||||
rvf_solver_result_len(h: number): number;
|
||||
rvf_solver_result_read(h: number, ptr: number): number;
|
||||
rvf_solver_policy_len(h: number): number;
|
||||
rvf_solver_policy_read(h: number, ptr: number): number;
|
||||
rvf_solver_witness_len(h: number): number;
|
||||
rvf_solver_witness_read(h: number, ptr: number): number;
|
||||
}
|
||||
|
||||
let wasmInstance: WasmExports | null = null;
|
||||
let loadPromise: Promise<WasmExports | null> | null = null;
|
||||
|
||||
async function loadWasm(): Promise<WasmExports | null> {
|
||||
try {
|
||||
const response = await fetch('/rvf_solver_wasm.wasm');
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const { instance } = await WebAssembly.instantiateStreaming(response, {
|
||||
env: {},
|
||||
});
|
||||
|
||||
return instance.exports as unknown as WasmExports;
|
||||
} catch (e) {
|
||||
console.debug('[rvf-solver] WASM load failed, using demo mode:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(wasm: WasmExports, handle: number, lenFn: (h: number) => number, readFn: (h: number, ptr: number) => number): unknown | null {
|
||||
const len = lenFn(handle);
|
||||
if (len <= 0) return null;
|
||||
const ptr = wasm.rvf_solver_alloc(len);
|
||||
if (ptr === 0) return null;
|
||||
try {
|
||||
readFn(handle, ptr);
|
||||
const buf = new Uint8Array(wasm.memory.buffer, ptr, len);
|
||||
const text = new TextDecoder().decode(buf);
|
||||
return JSON.parse(text);
|
||||
} finally {
|
||||
wasm.rvf_solver_free(ptr, len);
|
||||
}
|
||||
}
|
||||
|
||||
function splitSeed(seed?: number | bigint): [number, number] {
|
||||
if (seed === undefined) {
|
||||
const s = BigInt(Math.floor(Math.random() * 2 ** 64));
|
||||
return [Number(s & 0xffffffffn), Number((s >> 32n) & 0xffffffffn)];
|
||||
}
|
||||
const s = typeof seed === 'number' ? BigInt(seed) : seed;
|
||||
return [Number(s & 0xffffffffn), Number((s >> 32n) & 0xffffffffn)];
|
||||
}
|
||||
|
||||
/** Live WASM solver wrapper */
|
||||
class WasmSolver {
|
||||
private handle: number;
|
||||
private wasm: WasmExports;
|
||||
|
||||
constructor(handle: number, wasm: WasmExports) {
|
||||
this.handle = handle;
|
||||
this.wasm = wasm;
|
||||
}
|
||||
|
||||
train(options: { count: number; minDifficulty?: number; maxDifficulty?: number; seed?: number }): TrainResult {
|
||||
const [seedLo, seedHi] = splitSeed(options.seed);
|
||||
const correct = this.wasm.rvf_solver_train(
|
||||
this.handle, options.count,
|
||||
options.minDifficulty ?? 1, options.maxDifficulty ?? 10,
|
||||
seedLo, seedHi,
|
||||
);
|
||||
if (correct < 0) throw new Error('Training failed');
|
||||
|
||||
const raw = readJson(this.wasm, this.handle,
|
||||
(h) => this.wasm.rvf_solver_result_len(h),
|
||||
(h, p) => this.wasm.rvf_solver_result_read(h, p),
|
||||
) as { trained: number; correct: number; accuracy: number; patterns_learned?: number } | null;
|
||||
|
||||
return {
|
||||
trained: raw?.trained ?? options.count,
|
||||
correct: raw?.correct ?? correct,
|
||||
accuracy: raw?.accuracy ?? correct / options.count,
|
||||
patternsLearned: raw?.patterns_learned ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
acceptance(options?: { cycles?: number; holdoutSize?: number; trainingPerCycle?: number; stepBudget?: number; seed?: number }): AcceptanceManifest {
|
||||
const opts = options ?? {};
|
||||
const [seedLo, seedHi] = splitSeed(opts.seed);
|
||||
const status = this.wasm.rvf_solver_acceptance(
|
||||
this.handle,
|
||||
opts.holdoutSize ?? 50, opts.trainingPerCycle ?? 200,
|
||||
opts.cycles ?? 5, opts.stepBudget ?? 500,
|
||||
seedLo, seedHi,
|
||||
);
|
||||
if (status < 0) throw new Error('Acceptance failed');
|
||||
|
||||
const raw = readJson(this.wasm, this.handle,
|
||||
(h) => this.wasm.rvf_solver_result_len(h),
|
||||
(h, p) => this.wasm.rvf_solver_result_read(h, p),
|
||||
) as Record<string, unknown> | null;
|
||||
|
||||
if (!raw) throw new Error('Failed to read acceptance manifest');
|
||||
|
||||
const mapMode = (m: Record<string, unknown>): ModeResult => ({
|
||||
passed: !!m.passed,
|
||||
accuracyMaintained: !!(m.accuracy_maintained ?? m.accuracyMaintained),
|
||||
costImproved: !!(m.cost_improved ?? m.costImproved),
|
||||
robustnessImproved: !!(m.robustness_improved ?? m.robustnessImproved),
|
||||
zeroViolations: !!(m.zero_violations ?? m.zeroViolations),
|
||||
dimensionsImproved: (m.dimensions_improved ?? m.dimensionsImproved ?? 0) as number,
|
||||
cycles: ((m.cycles ?? []) as Record<string, unknown>[]).map((c) => ({
|
||||
cycle: (c.cycle ?? 0) as number,
|
||||
accuracy: (c.accuracy ?? 0) as number,
|
||||
costPerSolve: (c.cost_per_solve ?? c.costPerSolve ?? 0) as number,
|
||||
noiseAccuracy: (c.noise_accuracy ?? c.noiseAccuracy ?? 0) as number,
|
||||
violations: (c.violations ?? 0) as number,
|
||||
patternsLearned: (c.patterns_learned ?? c.patternsLearned ?? 0) as number,
|
||||
})),
|
||||
});
|
||||
|
||||
return {
|
||||
version: (raw.version ?? 2) as number,
|
||||
modeA: mapMode(raw.mode_a as Record<string, unknown>),
|
||||
modeB: mapMode(raw.mode_b as Record<string, unknown>),
|
||||
modeC: mapMode(raw.mode_c as Record<string, unknown>),
|
||||
allPassed: !!raw.all_passed,
|
||||
witnessEntries: (raw.witness_entries ?? 0) as number,
|
||||
witnessChainBytes: (raw.witness_chain_bytes ?? 0) as number,
|
||||
};
|
||||
}
|
||||
|
||||
policy(): PolicyState | null {
|
||||
const raw = readJson(this.wasm, this.handle,
|
||||
(h) => this.wasm.rvf_solver_policy_len(h),
|
||||
(h, p) => this.wasm.rvf_solver_policy_read(h, p),
|
||||
) as Record<string, unknown> | null;
|
||||
|
||||
if (!raw) return null;
|
||||
return {
|
||||
contextStats: (raw.context_stats ?? raw.contextStats ?? {}) as Record<string, Record<string, unknown>>,
|
||||
earlyCommitPenalties: (raw.early_commit_penalties ?? raw.earlyCommitPenalties ?? 0) as number,
|
||||
earlyCommitsTotal: (raw.early_commits_total ?? raw.earlyCommitsTotal ?? 0) as number,
|
||||
earlyCommitsWrong: (raw.early_commits_wrong ?? raw.earlyCommitsWrong ?? 0) as number,
|
||||
prepass: (raw.prepass ?? '') as string,
|
||||
speculativeAttempts: (raw.speculative_attempts ?? raw.speculativeAttempts ?? 0) as number,
|
||||
speculativeArm2Wins: (raw.speculative_arm2_wins ?? raw.speculativeArm2Wins ?? 0) as number,
|
||||
};
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.handle > 0) {
|
||||
this.wasm.rvf_solver_destroy(this.handle);
|
||||
this.handle = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
export interface SolverInterface {
|
||||
train(options: { count: number; minDifficulty?: number; maxDifficulty?: number; seed?: number }): TrainResult;
|
||||
acceptance(options?: { cycles?: number; holdoutSize?: number; trainingPerCycle?: number; stepBudget?: number; seed?: number }): AcceptanceManifest;
|
||||
policy(): PolicyState | null;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
let solverInstance: SolverInterface | null = null;
|
||||
let solverInitPromise: Promise<SolverInterface | null> | null = null;
|
||||
|
||||
async function initSolver(): Promise<SolverInterface | null> {
|
||||
if (!loadPromise) loadPromise = loadWasm();
|
||||
const wasm = await loadPromise;
|
||||
|
||||
if (!wasm) return null;
|
||||
|
||||
const handle = wasm.rvf_solver_create();
|
||||
if (handle < 0) {
|
||||
console.debug('[rvf-solver] Failed to create solver instance');
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WasmSolver(handle, wasm);
|
||||
}
|
||||
|
||||
export async function getSolver(): Promise<SolverInterface | null> {
|
||||
if (solverInstance) return solverInstance;
|
||||
if (!solverInitPromise) solverInitPromise = initSolver();
|
||||
solverInstance = await solverInitPromise;
|
||||
return solverInstance;
|
||||
}
|
||||
|
||||
/** Returns true if WASM solver is loaded. */
|
||||
export async function isWasmAvailable(): Promise<boolean> {
|
||||
const s = await getSolver();
|
||||
return s !== null;
|
||||
}
|
||||
|
||||
// ── Demo fallbacks ──
|
||||
|
||||
export function demoTrainResult(count: number, cycle: number): TrainResult {
|
||||
const baseAccuracy = 0.55 + cycle * 0.08;
|
||||
const accuracy = Math.min(0.98, baseAccuracy + (Math.random() - 0.5) * 0.04);
|
||||
const correct = Math.round(count * accuracy);
|
||||
return {
|
||||
trained: count,
|
||||
correct,
|
||||
accuracy,
|
||||
patternsLearned: Math.floor(count * 0.15 * (1 + cycle * 0.3)),
|
||||
};
|
||||
}
|
||||
|
||||
export function demoAcceptanceManifest(): AcceptanceManifest {
|
||||
const makeCycles = (baseAcc: number): CycleMetric[] =>
|
||||
Array.from({ length: 5 }, (_, i) => ({
|
||||
cycle: i + 1,
|
||||
accuracy: Math.min(0.99, baseAcc + i * 0.03 + (Math.random() - 0.5) * 0.02),
|
||||
costPerSolve: 120 - i * 15 + Math.random() * 10,
|
||||
noiseAccuracy: baseAcc - 0.05 + Math.random() * 0.03,
|
||||
violations: i < 2 ? 1 : 0,
|
||||
patternsLearned: (i + 1) * 12,
|
||||
}));
|
||||
|
||||
return {
|
||||
version: 2,
|
||||
modeA: { passed: true, accuracyMaintained: true, costImproved: false, robustnessImproved: false, zeroViolations: false, dimensionsImproved: 1, cycles: makeCycles(0.62) },
|
||||
modeB: { passed: true, accuracyMaintained: true, costImproved: true, robustnessImproved: false, zeroViolations: false, dimensionsImproved: 2, cycles: makeCycles(0.71) },
|
||||
modeC: { passed: true, accuracyMaintained: true, costImproved: true, robustnessImproved: true, zeroViolations: true, dimensionsImproved: 3, cycles: makeCycles(0.78) },
|
||||
allPassed: true,
|
||||
witnessEntries: 25,
|
||||
witnessChainBytes: 1825,
|
||||
};
|
||||
}
|
||||
|
||||
export function demoPolicyState(): PolicyState {
|
||||
const buckets = ['easy', 'medium', 'hard', 'extreme'];
|
||||
const modes = ['none', 'weekday', 'hybrid'];
|
||||
const contextStats: Record<string, Record<string, unknown>> = {};
|
||||
for (const bucket of buckets) {
|
||||
contextStats[bucket] = {};
|
||||
for (const mode of modes) {
|
||||
contextStats[bucket][mode] = {
|
||||
attempts: Math.floor(Math.random() * 200) + 50,
|
||||
successes: Math.floor(Math.random() * 150) + 30,
|
||||
totalSteps: Math.floor(Math.random() * 5000) + 1000,
|
||||
alphaSafety: 1.0 + Math.random() * 2,
|
||||
betaSafety: 1.0 + Math.random(),
|
||||
costEma: 50 + Math.random() * 80,
|
||||
earlyCommitWrongs: Math.floor(Math.random() * 5),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
contextStats,
|
||||
earlyCommitPenalties: 3,
|
||||
earlyCommitsTotal: 42,
|
||||
earlyCommitsWrong: 3,
|
||||
prepass: 'naked_singles',
|
||||
speculativeAttempts: 156,
|
||||
speculativeArm2Wins: 38,
|
||||
};
|
||||
}
|
||||
855
vendor/ruvector/examples/rvf/dashboard/src/styles/main.css
vendored
Normal file
855
vendor/ruvector/examples/rvf/dashboard/src/styles/main.css
vendored
Normal file
@@ -0,0 +1,855 @@
|
||||
/* Causal Atlas Dashboard - Scientific Instrument Theme */
|
||||
|
||||
:root {
|
||||
--bg: #0B0F14;
|
||||
--bg-panel: #11161C;
|
||||
--bg-surface: #151B23;
|
||||
--text-primary: #E6EDF3;
|
||||
--text-secondary: #8B949E;
|
||||
--text-muted: #484F58;
|
||||
--accent: #00E5FF;
|
||||
--accent-dim: rgba(0, 229, 255, 0.08);
|
||||
--accent-border: rgba(0, 229, 255, 0.2);
|
||||
--warning: #FFB020;
|
||||
--warning-dim: rgba(255, 176, 32, 0.1);
|
||||
--critical: #FF4D4D;
|
||||
--critical-dim: rgba(255, 77, 77, 0.1);
|
||||
--success: #2ECC71;
|
||||
--success-dim: rgba(46, 204, 113, 0.1);
|
||||
--border: #1E2630;
|
||||
--border-subtle: #161C24;
|
||||
--radius: 6px;
|
||||
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
/* ---- Layout ---- */
|
||||
|
||||
.view-container {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view-with-sidebar {
|
||||
grid-template-columns: 280px 1fr;
|
||||
}
|
||||
|
||||
.view-split {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.view-full {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* 12-column grid for dashboards */
|
||||
.grid-12 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.col-3 { grid-column: span 3; }
|
||||
.col-4 { grid-column: span 4; }
|
||||
.col-6 { grid-column: span 6; }
|
||||
.col-8 { grid-column: span 8; }
|
||||
.col-12 { grid-column: span 12; }
|
||||
|
||||
/* ---- Metric Cards ---- */
|
||||
|
||||
.metric-card {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metric-card .metric-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
}
|
||||
|
||||
.metric-card .metric-value {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.metric-card .metric-value.accent { color: var(--accent); }
|
||||
.metric-card .metric-value.warning { color: var(--warning); }
|
||||
.metric-card .metric-value.critical { color: var(--critical); }
|
||||
.metric-card .metric-value.success { color: var(--success); }
|
||||
|
||||
.metric-card .metric-sub {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.metric-card .metric-sub .trend-up { color: var(--success); }
|
||||
.metric-card .metric-sub .trend-down { color: var(--critical); }
|
||||
|
||||
.metric-card .sparkline {
|
||||
margin-top: 8px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.metric-card .sparkline canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ---- Panels ---- */
|
||||
|
||||
.panel {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px 16px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* ---- Sidebar ---- */
|
||||
|
||||
.sidebar {
|
||||
background: var(--bg-panel);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
}
|
||||
|
||||
.sidebar-search {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-sans);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sidebar-search::placeholder { color: var(--text-muted); }
|
||||
.sidebar-search:focus { border-bottom-color: var(--accent); }
|
||||
|
||||
.sidebar-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.sidebar-item:hover { background: rgba(255, 255, 255, 0.02); }
|
||||
|
||||
.sidebar-item.selected {
|
||||
background: var(--accent-dim);
|
||||
border-left: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.sidebar-item-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.sidebar-item-secondary {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ---- Canvas / Three.js ---- */
|
||||
|
||||
.three-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.three-container canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Scale selector overlay */
|
||||
.scale-selector {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.scale-btn {
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.scale-btn:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.scale-btn.active {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
/* ---- Charts ---- */
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chart-container h3 {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
}
|
||||
|
||||
/* ---- Tables ---- */
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
text-align: left;
|
||||
padding: 10px 14px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.data-table th:hover { color: var(--text-primary); }
|
||||
|
||||
.data-table td {
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.data-table tr:hover td {
|
||||
background: rgba(255, 255, 255, 0.015);
|
||||
}
|
||||
|
||||
.data-table tr.selected td {
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
/* ---- Progress bars ---- */
|
||||
|
||||
.progress-bar {
|
||||
height: 4px;
|
||||
background: var(--bg);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.progress-fill.accent { background: var(--accent); }
|
||||
.progress-fill.success { background: var(--success); }
|
||||
.progress-fill.warning { background: var(--warning); }
|
||||
.progress-fill.critical { background: var(--critical); }
|
||||
.progress-fill.info { background: var(--accent); }
|
||||
|
||||
.progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* ---- Gauges ---- */
|
||||
|
||||
.gauge-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.gauge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.gauge-ring {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 6px;
|
||||
}
|
||||
|
||||
.gauge-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.gauge-value {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ---- Color legend ---- */
|
||||
|
||||
.color-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.color-legend-bar {
|
||||
width: 100px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(to right, var(--accent), var(--critical));
|
||||
}
|
||||
|
||||
/* ---- Time scrubber ---- */
|
||||
|
||||
.time-scrubber {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.time-scrubber-title {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.time-scrubber-range {
|
||||
flex: 1;
|
||||
accent-color: var(--accent);
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.time-scrubber-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ---- Timeline strip ---- */
|
||||
|
||||
.timeline-strip {
|
||||
background: var(--bg-panel);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 12px 20px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.timeline-strip .timeline-label {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.timeline-strip canvas {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ---- Witness log ---- */
|
||||
|
||||
.witness-log {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.witness-log-header {
|
||||
padding: 10px 14px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.witness-log-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.witness-log-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 14px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.witness-log-entry:hover {
|
||||
background: rgba(255, 255, 255, 0.015);
|
||||
}
|
||||
|
||||
.witness-ts {
|
||||
color: var(--text-muted);
|
||||
min-width: 80px;
|
||||
white-space: nowrap;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.witness-step {
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.witness-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
min-width: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.witness-badge-commit { background: rgba(0, 229, 255, 0.1); color: var(--accent); }
|
||||
.witness-badge-verify { background: var(--success-dim); color: var(--success); }
|
||||
.witness-badge-seal { background: var(--critical-dim); color: var(--critical); }
|
||||
.witness-badge-merge { background: var(--warning-dim); color: var(--warning); }
|
||||
|
||||
.witness-hash {
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ---- Pipeline stages ---- */
|
||||
|
||||
.pipeline-stages {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pipeline-stage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 48px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.pipeline-stage.active {
|
||||
background: var(--success-dim);
|
||||
color: var(--success);
|
||||
border-color: rgba(46, 204, 113, 0.3);
|
||||
}
|
||||
|
||||
.pipeline-stage.pending {
|
||||
background: var(--warning-dim);
|
||||
color: var(--warning);
|
||||
border-color: rgba(255, 176, 32, 0.3);
|
||||
}
|
||||
|
||||
.pipeline-arrow {
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ---- Toolbar ---- */
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
background: var(--bg-panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.toolbar button:hover {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toolbar button.active {
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
|
||||
/* ---- Split panel layout ---- */
|
||||
|
||||
.split-layout {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border-right: 1px solid var(--border);
|
||||
width: 50%;
|
||||
min-width: 0;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.left-panel .table-area {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.left-panel .chart-area {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
width: 50%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ---- Detail floating panel ---- */
|
||||
|
||||
.detail-panel {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-panel h4 {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.detail-row:last-child { border-bottom: none; }
|
||||
|
||||
.detail-key {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.detail-val {
|
||||
font-size: 12px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---- Score badge ---- */
|
||||
|
||||
.score-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.score-high { background: var(--success-dim); color: var(--success); }
|
||||
.score-medium { background: var(--warning-dim); color: var(--warning); }
|
||||
.score-low { background: var(--critical-dim); color: var(--critical); }
|
||||
|
||||
/* ---- Dashboard grid layouts ---- */
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dashboard-grid .full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-auto-rows: min-content;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.status-grid .full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* ---- Boundary tick marks ---- */
|
||||
|
||||
.boundary-tick {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
background: var(--warning);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ---- Alert item ---- */
|
||||
|
||||
.alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.alert-item:last-child { border-bottom: none; }
|
||||
|
||||
.alert-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.alert-dot.warning { background: var(--warning); }
|
||||
.alert-dot.critical { background: var(--critical); }
|
||||
.alert-dot.success { background: var(--success); }
|
||||
|
||||
.alert-msg {
|
||||
flex: 1;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.alert-sector {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ---- Empty / loading states ---- */
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* ---- Scrollbar ---- */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(11, 15, 20, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 229, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(0, 229, 255, 0.08);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 229, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Firefox scrollbar */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 229, 255, 0.2) rgba(11, 15, 20, 0.5);
|
||||
}
|
||||
|
||||
/* ---- Responsive ---- */
|
||||
|
||||
@media (max-width: 1279px) {
|
||||
.view-split { grid-template-columns: 1fr; grid-template-rows: 1fr 1fr; }
|
||||
.split-layout { flex-direction: column; }
|
||||
.left-panel, .right-panel { width: 100%; }
|
||||
.left-panel { border-right: none; border-bottom: 1px solid var(--border); }
|
||||
.col-3 { grid-column: span 6; }
|
||||
.col-4 { grid-column: span 6; }
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.view-with-sidebar { grid-template-columns: 1fr; }
|
||||
.sidebar { max-height: 200px; border-right: none; border-bottom: 1px solid var(--border); }
|
||||
.dashboard-grid, .status-grid { grid-template-columns: 1fr; }
|
||||
.col-3, .col-4, .col-6 { grid-column: span 12; }
|
||||
}
|
||||
199
vendor/ruvector/examples/rvf/dashboard/src/three/AtlasGraph.ts
vendored
Normal file
199
vendor/ruvector/examples/rvf/dashboard/src/three/AtlasGraph.ts
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
domain: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
const DOMAIN_COLORS: Record<string, THREE.Color> = {
|
||||
transit: new THREE.Color(0x00E5FF),
|
||||
flare: new THREE.Color(0xFF4D4D),
|
||||
rotation: new THREE.Color(0x2ECC71),
|
||||
eclipse: new THREE.Color(0x9944ff),
|
||||
variability: new THREE.Color(0xFFB020),
|
||||
};
|
||||
|
||||
const DEFAULT_COLOR = new THREE.Color(0x8B949E);
|
||||
|
||||
function colorForDomain(domain: string): THREE.Color {
|
||||
return DOMAIN_COLORS[domain] ?? DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
export class AtlasGraph {
|
||||
private nodesMesh: THREE.InstancedMesh | null = null;
|
||||
private edgesLine: THREE.LineSegments | null = null;
|
||||
private glowPoints: THREE.Points | null = null;
|
||||
private scene: THREE.Scene;
|
||||
private nodeMap: Map<string, number> = new Map();
|
||||
|
||||
constructor(scene: THREE.Scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
setNodes(nodes: GraphNode[]): void {
|
||||
this.disposeNodes();
|
||||
|
||||
// Star-like nodes using InstancedMesh with emissive material
|
||||
const geometry = new THREE.SphereGeometry(0.12, 8, 6);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
vertexColors: false,
|
||||
emissiveIntensity: 0.8,
|
||||
roughness: 0.3,
|
||||
metalness: 0.1,
|
||||
});
|
||||
const mesh = new THREE.InstancedMesh(geometry, material, nodes.length);
|
||||
|
||||
const dummy = new THREE.Object3D();
|
||||
const color = new THREE.Color();
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
this.nodeMap.set(node.id, i);
|
||||
|
||||
dummy.position.set(node.x, node.y, node.z);
|
||||
const scale = 0.3 + node.weight * 0.7;
|
||||
dummy.scale.set(scale, scale, scale);
|
||||
dummy.updateMatrix();
|
||||
mesh.setMatrixAt(i, dummy.matrix);
|
||||
|
||||
color.copy(colorForDomain(node.domain));
|
||||
mesh.setColorAt(i, color);
|
||||
}
|
||||
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
|
||||
|
||||
this.nodesMesh = mesh;
|
||||
this.scene.add(mesh);
|
||||
|
||||
// Additive glow halo points around each node
|
||||
const glowPositions = new Float32Array(nodes.length * 3);
|
||||
const glowColors = new Float32Array(nodes.length * 3);
|
||||
const glowSizes = new Float32Array(nodes.length);
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
glowPositions[i * 3] = node.x;
|
||||
glowPositions[i * 3 + 1] = node.y;
|
||||
glowPositions[i * 3 + 2] = node.z;
|
||||
|
||||
const c = colorForDomain(node.domain);
|
||||
glowColors[i * 3] = c.r;
|
||||
glowColors[i * 3 + 1] = c.g;
|
||||
glowColors[i * 3 + 2] = c.b;
|
||||
|
||||
glowSizes[i] = 0.8 + node.weight * 1.5;
|
||||
}
|
||||
|
||||
const glowGeo = new THREE.BufferGeometry();
|
||||
glowGeo.setAttribute('position', new THREE.Float32BufferAttribute(glowPositions, 3));
|
||||
glowGeo.setAttribute('color', new THREE.Float32BufferAttribute(glowColors, 3));
|
||||
|
||||
const glowMat = new THREE.PointsMaterial({
|
||||
size: 1.2,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.25,
|
||||
sizeAttenuation: true,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending,
|
||||
});
|
||||
|
||||
this.glowPoints = new THREE.Points(glowGeo, glowMat);
|
||||
this.scene.add(this.glowPoints);
|
||||
}
|
||||
|
||||
setEdges(edges: GraphEdge[], nodes: GraphNode[]): void {
|
||||
this.disposeEdges();
|
||||
|
||||
const positions: number[] = [];
|
||||
const colors: number[] = [];
|
||||
|
||||
const nodeById = new Map<string, GraphNode>();
|
||||
for (const n of nodes) nodeById.set(n.id, n);
|
||||
|
||||
for (const edge of edges) {
|
||||
const src = nodeById.get(edge.source);
|
||||
const tgt = nodeById.get(edge.target);
|
||||
if (!src || !tgt) continue;
|
||||
|
||||
positions.push(src.x, src.y, src.z);
|
||||
positions.push(tgt.x, tgt.y, tgt.z);
|
||||
|
||||
// Cyan glow edges with weight-based opacity
|
||||
const alpha = Math.max(0.05, Math.min(0.6, edge.weight * 0.5));
|
||||
colors.push(0.0, 0.9, 1.0, alpha);
|
||||
colors.push(0.0, 0.9, 1.0, alpha);
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 4));
|
||||
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending,
|
||||
});
|
||||
|
||||
this.edgesLine = new THREE.LineSegments(geometry, material);
|
||||
this.scene.add(this.edgesLine);
|
||||
}
|
||||
|
||||
getNodeIndex(id: string): number | undefined {
|
||||
return this.nodeMap.get(id);
|
||||
}
|
||||
|
||||
/** Animate node glow pulse (0-1 range). */
|
||||
setPulse(intensity: number): void {
|
||||
if (this.glowPoints) {
|
||||
(this.glowPoints.material as THREE.PointsMaterial).opacity = 0.15 + intensity * 0.15;
|
||||
}
|
||||
if (this.nodesMesh) {
|
||||
const mat = this.nodesMesh.material as THREE.MeshStandardMaterial;
|
||||
mat.emissiveIntensity = 0.5 + intensity * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
private disposeNodes(): void {
|
||||
if (this.nodesMesh) {
|
||||
this.scene.remove(this.nodesMesh);
|
||||
this.nodesMesh.geometry.dispose();
|
||||
(this.nodesMesh.material as THREE.Material).dispose();
|
||||
this.nodesMesh = null;
|
||||
}
|
||||
if (this.glowPoints) {
|
||||
this.scene.remove(this.glowPoints);
|
||||
this.glowPoints.geometry.dispose();
|
||||
(this.glowPoints.material as THREE.Material).dispose();
|
||||
this.glowPoints = null;
|
||||
}
|
||||
this.nodeMap.clear();
|
||||
}
|
||||
|
||||
private disposeEdges(): void {
|
||||
if (this.edgesLine) {
|
||||
this.scene.remove(this.edgesLine);
|
||||
this.edgesLine.geometry.dispose();
|
||||
(this.edgesLine.material as THREE.Material).dispose();
|
||||
this.edgesLine = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposeNodes();
|
||||
this.disposeEdges();
|
||||
}
|
||||
}
|
||||
248
vendor/ruvector/examples/rvf/dashboard/src/three/CoherenceSurface.ts
vendored
Normal file
248
vendor/ruvector/examples/rvf/dashboard/src/three/CoherenceSurface.ts
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class CoherenceSurface {
|
||||
private mesh: THREE.Mesh | null = null;
|
||||
private wireframe: THREE.LineSegments | null = null;
|
||||
private contourLines: THREE.Group | null = null;
|
||||
private gridLabels: THREE.Group | null = null;
|
||||
private scene: THREE.Scene;
|
||||
private gridWidth: number;
|
||||
private gridHeight: number;
|
||||
|
||||
constructor(scene: THREE.Scene, gridWidth = 64, gridHeight = 64) {
|
||||
this.scene = scene;
|
||||
this.gridWidth = gridWidth;
|
||||
this.gridHeight = gridHeight;
|
||||
this.createMesh();
|
||||
this.createGridLabels();
|
||||
}
|
||||
|
||||
private createMesh(): void {
|
||||
const geometry = new THREE.PlaneGeometry(
|
||||
10, 10,
|
||||
this.gridWidth - 1,
|
||||
this.gridHeight - 1,
|
||||
);
|
||||
|
||||
const vertexCount = geometry.attributes.position.count;
|
||||
const colors = new Float32Array(vertexCount * 3);
|
||||
for (let i = 0; i < vertexCount; i++) {
|
||||
colors[i * 3] = 0.0;
|
||||
colors[i * 3 + 1] = 0.3;
|
||||
colors[i * 3 + 2] = 0.5;
|
||||
}
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
vertexColors: true,
|
||||
side: THREE.DoubleSide,
|
||||
shininess: 40,
|
||||
specular: new THREE.Color(0x112233),
|
||||
flatShading: false,
|
||||
});
|
||||
|
||||
this.mesh = new THREE.Mesh(geometry, material);
|
||||
this.mesh.rotation.x = -Math.PI / 2;
|
||||
this.scene.add(this.mesh);
|
||||
|
||||
// Subtle grid overlay
|
||||
const wireGeo = new THREE.WireframeGeometry(geometry);
|
||||
const wireMat = new THREE.LineBasicMaterial({
|
||||
color: 0x1C2333,
|
||||
transparent: true,
|
||||
opacity: 0.12,
|
||||
});
|
||||
this.wireframe = new THREE.LineSegments(wireGeo, wireMat);
|
||||
this.wireframe.rotation.x = -Math.PI / 2;
|
||||
this.scene.add(this.wireframe);
|
||||
}
|
||||
|
||||
private createGridLabels(): void {
|
||||
this.gridLabels = new THREE.Group();
|
||||
|
||||
// Base grid plane at y=0 with faint lines
|
||||
const gridHelper = new THREE.GridHelper(10, 8, 0x1C2333, 0x131A22);
|
||||
gridHelper.position.y = -0.01;
|
||||
this.gridLabels.add(gridHelper);
|
||||
|
||||
// Axis lines
|
||||
const axisMat = new THREE.LineBasicMaterial({ color: 0x2A3444, transparent: true, opacity: 0.5 });
|
||||
const xAxisGeo = new THREE.BufferGeometry().setFromPoints([
|
||||
new THREE.Vector3(-5.5, 0, 5.5),
|
||||
new THREE.Vector3(5.5, 0, 5.5),
|
||||
]);
|
||||
this.gridLabels.add(new THREE.Line(xAxisGeo, axisMat));
|
||||
|
||||
const zAxisGeo = new THREE.BufferGeometry().setFromPoints([
|
||||
new THREE.Vector3(-5.5, 0, 5.5),
|
||||
new THREE.Vector3(-5.5, 0, -5.5),
|
||||
]);
|
||||
this.gridLabels.add(new THREE.Line(zAxisGeo, axisMat));
|
||||
|
||||
this.scene.add(this.gridLabels);
|
||||
}
|
||||
|
||||
/** Map coherence value [0,1] to a clear multi-stop color ramp. */
|
||||
private valueToColor(v: number, color: THREE.Color): void {
|
||||
// 1.0 = deep stable blue, 0.85 = cyan, 0.75 = yellow warning, <0.7 = red critical
|
||||
if (v > 0.85) {
|
||||
// Blue -> Cyan (stable zone)
|
||||
const t = (v - 0.85) / 0.15;
|
||||
color.setRGB(0.0, 0.4 + t * 0.1, 0.6 + t * 0.4);
|
||||
} else if (v > 0.75) {
|
||||
// Cyan -> Yellow (transition)
|
||||
const t = (v - 0.75) / 0.1;
|
||||
color.setRGB(1.0 - t * 1.0, 0.7 + t * 0.2, t * 0.6);
|
||||
} else if (v > 0.65) {
|
||||
// Yellow -> Orange (warning)
|
||||
const t = (v - 0.65) / 0.1;
|
||||
color.setRGB(1.0, 0.5 + t * 0.2, t * 0.1);
|
||||
} else {
|
||||
// Orange -> Red (critical)
|
||||
const t = Math.max(0, v / 0.65);
|
||||
color.setRGB(0.9 + t * 0.1, 0.15 + t * 0.35, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
setValues(values: number[]): void {
|
||||
if (!this.mesh) return;
|
||||
|
||||
const geometry = this.mesh.geometry;
|
||||
const colorAttr = geometry.attributes.color;
|
||||
const posAttr = geometry.attributes.position;
|
||||
const count = Math.min(values.length, colorAttr.count);
|
||||
|
||||
const color = new THREE.Color();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const v = Math.max(0, Math.min(1, values[i]));
|
||||
this.valueToColor(v, color);
|
||||
colorAttr.setXYZ(i, color.r, color.g, color.b);
|
||||
|
||||
// Elevation: higher coherence = flat, lower = raised (shows "pressure")
|
||||
const elevation = (1 - v) * 2.5;
|
||||
posAttr.setZ(i, elevation);
|
||||
}
|
||||
|
||||
colorAttr.needsUpdate = true;
|
||||
posAttr.needsUpdate = true;
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
this.updateWireframe(geometry);
|
||||
this.updateContours(values);
|
||||
}
|
||||
|
||||
private updateWireframe(geometry: THREE.PlaneGeometry): void {
|
||||
if (this.wireframe) {
|
||||
this.scene.remove(this.wireframe);
|
||||
this.wireframe.geometry.dispose();
|
||||
(this.wireframe.material as THREE.Material).dispose();
|
||||
}
|
||||
|
||||
const wireGeo = new THREE.WireframeGeometry(geometry);
|
||||
const wireMat = new THREE.LineBasicMaterial({
|
||||
color: 0x1C2333,
|
||||
transparent: true,
|
||||
opacity: 0.12,
|
||||
});
|
||||
this.wireframe = new THREE.LineSegments(wireGeo, wireMat);
|
||||
this.wireframe.rotation.x = -Math.PI / 2;
|
||||
this.scene.add(this.wireframe);
|
||||
}
|
||||
|
||||
/** Draw contour rings at threshold boundaries (0.8 warning, 0.7 critical). */
|
||||
private updateContours(values: number[]): void {
|
||||
if (this.contourLines) {
|
||||
this.scene.remove(this.contourLines);
|
||||
this.contourLines.traverse((obj) => {
|
||||
if (obj instanceof THREE.Line) {
|
||||
obj.geometry.dispose();
|
||||
(obj.material as THREE.Material).dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.contourLines = new THREE.Group();
|
||||
const gw = this.gridWidth;
|
||||
const gh = this.gridHeight;
|
||||
const halfW = 5;
|
||||
|
||||
const thresholds = [
|
||||
{ level: 0.80, color: 0xFFB020, opacity: 0.6 }, // warning
|
||||
{ level: 0.70, color: 0xFF4D4D, opacity: 0.7 }, // critical
|
||||
];
|
||||
|
||||
for (const thresh of thresholds) {
|
||||
const points: THREE.Vector3[] = [];
|
||||
|
||||
for (let y = 0; y < gh - 1; y++) {
|
||||
for (let x = 0; x < gw - 1; x++) {
|
||||
const v00 = values[y * gw + x] ?? 1;
|
||||
const v10 = values[y * gw + x + 1] ?? 1;
|
||||
const v01 = values[(y + 1) * gw + x] ?? 1;
|
||||
|
||||
// Horizontal edge crossing
|
||||
if ((v00 - thresh.level) * (v10 - thresh.level) < 0) {
|
||||
const t = (thresh.level - v00) / (v10 - v00);
|
||||
const wx = -halfW + ((x + t) / (gw - 1)) * halfW * 2;
|
||||
const wz = -halfW + (y / (gh - 1)) * halfW * 2;
|
||||
const elev = (1 - thresh.level) * 2.5;
|
||||
points.push(new THREE.Vector3(wx, elev + 0.02, wz));
|
||||
}
|
||||
|
||||
// Vertical edge crossing
|
||||
if ((v00 - thresh.level) * (v01 - thresh.level) < 0) {
|
||||
const t = (thresh.level - v00) / (v01 - v00);
|
||||
const wx = -halfW + (x / (gw - 1)) * halfW * 2;
|
||||
const wz = -halfW + ((y + t) / (gh - 1)) * halfW * 2;
|
||||
const elev = (1 - thresh.level) * 2.5;
|
||||
points.push(new THREE.Vector3(wx, elev + 0.02, wz));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (points.length > 1) {
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const mat = new THREE.PointsMaterial({
|
||||
color: thresh.color,
|
||||
size: 0.08,
|
||||
transparent: true,
|
||||
opacity: thresh.opacity,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.contourLines.add(new THREE.Points(geo, mat));
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.add(this.contourLines);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.mesh) {
|
||||
this.scene.remove(this.mesh);
|
||||
this.mesh.geometry.dispose();
|
||||
(this.mesh.material as THREE.Material).dispose();
|
||||
this.mesh = null;
|
||||
}
|
||||
if (this.wireframe) {
|
||||
this.scene.remove(this.wireframe);
|
||||
this.wireframe.geometry.dispose();
|
||||
(this.wireframe.material as THREE.Material).dispose();
|
||||
this.wireframe = null;
|
||||
}
|
||||
if (this.contourLines) {
|
||||
this.scene.remove(this.contourLines);
|
||||
this.contourLines.traverse((obj) => {
|
||||
if (obj instanceof THREE.Line || obj instanceof THREE.Points) {
|
||||
obj.geometry.dispose();
|
||||
(obj.material as THREE.Material).dispose();
|
||||
}
|
||||
});
|
||||
this.contourLines = null;
|
||||
}
|
||||
if (this.gridLabels) {
|
||||
this.scene.remove(this.gridLabels);
|
||||
this.gridLabels = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
390
vendor/ruvector/examples/rvf/dashboard/src/three/DysonSphere3D.ts
vendored
Normal file
390
vendor/ruvector/examples/rvf/dashboard/src/three/DysonSphere3D.ts
vendored
Normal file
@@ -0,0 +1,390 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
/**
|
||||
* Interactive 3D Dyson sphere visualization with galactic context.
|
||||
*
|
||||
* Renders:
|
||||
* - Deep-field starfield + galactic plane
|
||||
* - Central star (emissive sphere, color from spectral type)
|
||||
* - Partial Dyson swarm shell (coverage_fraction controls opacity mask)
|
||||
* - IR waste heat glow halo
|
||||
* - Orbiting collector panels as instanced quads
|
||||
*
|
||||
* Interaction:
|
||||
* - OrbitControls: drag to rotate, scroll to zoom, right-drag to pan
|
||||
* - Speed control via setSpeed()
|
||||
* - Reset view via resetCamera()
|
||||
*/
|
||||
|
||||
export interface DysonParams {
|
||||
coverageFraction: number;
|
||||
warmTempK: number;
|
||||
spectralType: string;
|
||||
w3Excess: number;
|
||||
w4Excess: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const SPECTRAL_COLORS: Record<string, number> = {
|
||||
O: 0x9bb0ff, B: 0xaabfff, A: 0xcad7ff, F: 0xf8f7ff,
|
||||
G: 0xfff4ea, K: 0xffd2a1, M: 0xffb56c, L: 0xff8833,
|
||||
};
|
||||
|
||||
function starColor(spectralType: string): number {
|
||||
const letter = spectralType.charAt(0).toUpperCase();
|
||||
return SPECTRAL_COLORS[letter] ?? 0xffd2a1;
|
||||
}
|
||||
|
||||
function warmColor(tempK: number): THREE.Color {
|
||||
const t = Math.max(0, Math.min(1, (tempK - 100) / 400));
|
||||
return new THREE.Color().setHSL(0.02 + t * 0.06, 0.9, 0.3 + t * 0.2);
|
||||
}
|
||||
|
||||
function seededRandom(seed: number): () => number {
|
||||
let s = seed;
|
||||
return () => {
|
||||
s = (s * 16807 + 0) % 2147483647;
|
||||
return (s - 1) / 2147483646;
|
||||
};
|
||||
}
|
||||
|
||||
export class DysonSphere3D {
|
||||
private scene: THREE.Scene;
|
||||
private camera: THREE.PerspectiveCamera;
|
||||
private renderer: THREE.WebGLRenderer;
|
||||
private controls: OrbitControls;
|
||||
private starMesh: THREE.Mesh | null = null;
|
||||
private shellMesh: THREE.Mesh | null = null;
|
||||
private glowMesh: THREE.Mesh | null = null;
|
||||
private panelInstances: THREE.InstancedMesh | null = null;
|
||||
private animId = 0;
|
||||
private time = 0;
|
||||
private speedMultiplier = 1;
|
||||
private autoRotate = true;
|
||||
private defaultCamPos = new THREE.Vector3(0, 1.5, 4);
|
||||
private bgGroup: THREE.Group | null = null;
|
||||
|
||||
constructor(private container: HTMLElement) {
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x020408);
|
||||
|
||||
const w = container.clientWidth || 400;
|
||||
const h = container.clientHeight || 300;
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(50, w / h, 0.01, 2000);
|
||||
this.camera.position.set(0, 1.5, 4);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
this.renderer.setSize(w, h);
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
container.appendChild(this.renderer.domElement);
|
||||
|
||||
// OrbitControls
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.08;
|
||||
this.controls.minDistance = 1;
|
||||
this.controls.maxDistance = 400;
|
||||
this.controls.enablePan = true;
|
||||
this.controls.zoomSpeed = 1.2;
|
||||
this.controls.rotateSpeed = 0.8;
|
||||
this.controls.addEventListener('start', () => { this.autoRotate = false; });
|
||||
|
||||
this.scene.add(new THREE.AmbientLight(0x222244, 0.3));
|
||||
this.buildBackground();
|
||||
}
|
||||
|
||||
// ── Public controls ──
|
||||
|
||||
setSpeed(multiplier: number): void {
|
||||
this.speedMultiplier = multiplier;
|
||||
}
|
||||
|
||||
resetCamera(): void {
|
||||
this.autoRotate = true;
|
||||
this.camera.position.copy(this.defaultCamPos);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
toggleAutoRotate(): void {
|
||||
this.autoRotate = !this.autoRotate;
|
||||
}
|
||||
|
||||
getAutoRotate(): boolean {
|
||||
return this.autoRotate;
|
||||
}
|
||||
|
||||
// ── Background ──
|
||||
|
||||
private buildBackground(): void {
|
||||
this.bgGroup = new THREE.Group();
|
||||
const rand = seededRandom(77);
|
||||
|
||||
// Starfield
|
||||
const starCount = 4000;
|
||||
const positions = new Float32Array(starCount * 3);
|
||||
const colors = new Float32Array(starCount * 3);
|
||||
const tints = [
|
||||
new THREE.Color(0xffffff), new THREE.Color(0xaaccff),
|
||||
new THREE.Color(0xfff4ea), new THREE.Color(0xffd2a1),
|
||||
new THREE.Color(0xffb56c), new THREE.Color(0xccddff),
|
||||
];
|
||||
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
const theta = rand() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * rand() - 1);
|
||||
const r = 300 + rand() * 500;
|
||||
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
||||
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
||||
positions[i * 3 + 2] = r * Math.cos(phi);
|
||||
const tint = tints[Math.floor(rand() * tints.length)];
|
||||
const b = 0.4 + rand() * 0.6;
|
||||
colors[i * 3] = tint.r * b;
|
||||
colors[i * 3 + 1] = tint.g * b;
|
||||
colors[i * 3 + 2] = tint.b * b;
|
||||
}
|
||||
|
||||
const sGeo = new THREE.BufferGeometry();
|
||||
sGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
sGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
this.bgGroup.add(new THREE.Points(sGeo, new THREE.PointsMaterial({
|
||||
size: 1.5, vertexColors: true, transparent: true, opacity: 0.9,
|
||||
sizeAttenuation: true, depthWrite: false,
|
||||
})));
|
||||
|
||||
// Galactic plane
|
||||
const galCount = 5000;
|
||||
const gp = new Float32Array(galCount * 3);
|
||||
const gc = new Float32Array(galCount * 3);
|
||||
for (let i = 0; i < galCount; i++) {
|
||||
const a = rand() * Math.PI * 2;
|
||||
const d = Math.pow(rand(), 0.5) * 500;
|
||||
const h = (rand() - 0.5) * (12 + d * 0.02);
|
||||
gp[i * 3] = d * Math.cos(a);
|
||||
gp[i * 3 + 1] = h;
|
||||
gp[i * 3 + 2] = d * Math.sin(a);
|
||||
const cp = 1 - Math.min(1, d / 500);
|
||||
gc[i * 3] = 0.5 + cp * 0.35;
|
||||
gc[i * 3 + 1] = 0.5 + cp * 0.25;
|
||||
gc[i * 3 + 2] = 0.6 + rand() * 0.1;
|
||||
}
|
||||
const gGeo = new THREE.BufferGeometry();
|
||||
gGeo.setAttribute('position', new THREE.BufferAttribute(gp, 3));
|
||||
gGeo.setAttribute('color', new THREE.BufferAttribute(gc, 3));
|
||||
const gal = new THREE.Points(gGeo, new THREE.PointsMaterial({
|
||||
size: 0.8, vertexColors: true, transparent: true, opacity: 0.2,
|
||||
sizeAttenuation: true, depthWrite: false,
|
||||
}));
|
||||
gal.rotation.x = Math.PI * 0.35;
|
||||
gal.rotation.z = Math.PI * 0.15;
|
||||
gal.position.set(0, 80, -180);
|
||||
this.bgGroup.add(gal);
|
||||
|
||||
// Nebulae
|
||||
const nebColors = [0x3344aa, 0xaa3355, 0x2288aa, 0x8844aa];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128; canvas.height = 128;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const grad = ctx.createRadialGradient(64, 64, 4, 64, 64, 64);
|
||||
const col = new THREE.Color(nebColors[i % nebColors.length]);
|
||||
grad.addColorStop(0, `rgba(${Math.floor(col.r * 255)},${Math.floor(col.g * 255)},${Math.floor(col.b * 255)},0.25)`);
|
||||
grad.addColorStop(0.4, `rgba(${Math.floor(col.r * 255)},${Math.floor(col.g * 255)},${Math.floor(col.b * 255)},0.06)`);
|
||||
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 128, 128);
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false });
|
||||
const sp = new THREE.Sprite(mat);
|
||||
const t2 = rand() * Math.PI * 2;
|
||||
const p2 = (rand() - 0.5) * Math.PI * 0.5;
|
||||
const r2 = 200 + rand() * 350;
|
||||
sp.position.set(r2 * Math.cos(p2) * Math.cos(t2), r2 * Math.sin(p2), r2 * Math.cos(p2) * Math.sin(t2));
|
||||
sp.scale.setScalar(50 + rand() * 100);
|
||||
this.bgGroup.add(sp);
|
||||
}
|
||||
|
||||
this.scene.add(this.bgGroup);
|
||||
}
|
||||
|
||||
update(params: DysonParams): void {
|
||||
this.clearSystem();
|
||||
|
||||
const sc = starColor(params.spectralType);
|
||||
|
||||
// ── Central Star ──
|
||||
const starGeo = new THREE.SphereGeometry(0.5, 32, 32);
|
||||
const starMat = new THREE.MeshBasicMaterial({ color: sc });
|
||||
this.starMesh = new THREE.Mesh(starGeo, starMat);
|
||||
this.scene.add(this.starMesh);
|
||||
|
||||
const starLight = new THREE.PointLight(sc, 2, 20);
|
||||
starLight.position.set(0, 0, 0);
|
||||
this.scene.add(starLight);
|
||||
|
||||
// ── Dyson Shell ──
|
||||
const shellRadius = 1.5;
|
||||
const shellGeo = new THREE.SphereGeometry(shellRadius, 64, 64);
|
||||
const wc = warmColor(params.warmTempK);
|
||||
|
||||
const positions = shellGeo.attributes.position;
|
||||
const vertColors = new Float32Array(positions.count * 4);
|
||||
const coverage = params.coverageFraction;
|
||||
|
||||
for (let i = 0; i < positions.count; i++) {
|
||||
const x = positions.getX(i);
|
||||
const y = positions.getY(i);
|
||||
const z = positions.getZ(i);
|
||||
const theta = Math.atan2(Math.sqrt(x * x + z * z), y);
|
||||
const phi = Math.atan2(z, x);
|
||||
const pattern =
|
||||
0.5 + 0.2 * Math.sin(theta * 5 + phi * 3) +
|
||||
0.15 * Math.sin(theta * 8 - phi * 5) +
|
||||
0.15 * Math.cos(phi * 7 + theta * 2);
|
||||
const visible = pattern < coverage;
|
||||
const alpha = visible ? 0.6 + coverage * 0.3 : 0.02;
|
||||
vertColors[i * 4] = visible ? wc.r : 0.05;
|
||||
vertColors[i * 4 + 1] = visible ? wc.g : 0.05;
|
||||
vertColors[i * 4 + 2] = visible ? wc.b : 0.05;
|
||||
vertColors[i * 4 + 3] = alpha;
|
||||
}
|
||||
shellGeo.setAttribute('color', new THREE.BufferAttribute(vertColors, 4));
|
||||
|
||||
const shellMat = new THREE.MeshBasicMaterial({
|
||||
vertexColors: true, transparent: true, opacity: 0.7,
|
||||
side: THREE.DoubleSide, depthWrite: false,
|
||||
});
|
||||
this.shellMesh = new THREE.Mesh(shellGeo, shellMat);
|
||||
this.scene.add(this.shellMesh);
|
||||
|
||||
// Wireframe overlay
|
||||
const wireGeo = new THREE.SphereGeometry(shellRadius + 0.01, 24, 24);
|
||||
const wireMat = new THREE.MeshBasicMaterial({ color: wc, transparent: true, opacity: 0.08, wireframe: true });
|
||||
this.scene.add(new THREE.Mesh(wireGeo, wireMat));
|
||||
|
||||
// ── IR Glow ──
|
||||
const glowRadius = shellRadius + 0.3 + params.w4Excess * 0.1;
|
||||
const glowGeo = new THREE.SphereGeometry(glowRadius, 32, 32);
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: wc, transparent: true, opacity: 0.04 + coverage * 0.06,
|
||||
side: THREE.BackSide, depthWrite: false,
|
||||
});
|
||||
this.glowMesh = new THREE.Mesh(glowGeo, glowMat);
|
||||
this.scene.add(this.glowMesh);
|
||||
|
||||
// ── Collector Panels ──
|
||||
const panelCount = Math.floor(coverage * 400);
|
||||
if (panelCount > 0) {
|
||||
const panelGeo = new THREE.PlaneGeometry(0.06, 0.06);
|
||||
const panelMat = new THREE.MeshBasicMaterial({ color: wc, transparent: true, opacity: 0.9, side: THREE.DoubleSide });
|
||||
this.panelInstances = new THREE.InstancedMesh(panelGeo, panelMat, panelCount);
|
||||
|
||||
const dummy = new THREE.Object3D();
|
||||
for (let i = 0; i < panelCount; i++) {
|
||||
const t = i / panelCount;
|
||||
const incl = Math.acos(1 - 2 * t);
|
||||
const azim = Math.PI * (1 + Math.sqrt(5)) * i;
|
||||
const r = shellRadius + 0.02 + Math.random() * 0.05;
|
||||
dummy.position.set(
|
||||
r * Math.sin(incl) * Math.cos(azim),
|
||||
r * Math.cos(incl),
|
||||
r * Math.sin(incl) * Math.sin(azim),
|
||||
);
|
||||
dummy.lookAt(0, 0, 0);
|
||||
dummy.updateMatrix();
|
||||
this.panelInstances.setMatrixAt(i, dummy.matrix);
|
||||
}
|
||||
this.panelInstances.instanceMatrix.needsUpdate = true;
|
||||
this.scene.add(this.panelInstances);
|
||||
}
|
||||
|
||||
this.defaultCamPos.set(2.5, 1.5, 3.5);
|
||||
this.camera.position.copy(this.defaultCamPos);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
this.controls.update();
|
||||
this.autoRotate = true;
|
||||
|
||||
this.animate();
|
||||
}
|
||||
|
||||
private clearSystem(): void {
|
||||
cancelAnimationFrame(this.animId);
|
||||
const toRemove: THREE.Object3D[] = [];
|
||||
this.scene.traverse((obj) => {
|
||||
if (obj !== this.scene && obj !== this.bgGroup && obj.parent === this.scene && !(obj instanceof THREE.AmbientLight)) {
|
||||
toRemove.push(obj);
|
||||
}
|
||||
});
|
||||
for (const obj of toRemove) {
|
||||
this.scene.remove(obj);
|
||||
if ((obj as THREE.Mesh).geometry) (obj as THREE.Mesh).geometry.dispose();
|
||||
}
|
||||
let hasAmbient = false;
|
||||
this.scene.traverse((o) => { if (o instanceof THREE.AmbientLight) hasAmbient = true; });
|
||||
if (!hasAmbient) this.scene.add(new THREE.AmbientLight(0x222244, 0.3));
|
||||
|
||||
this.starMesh = null;
|
||||
this.shellMesh = null;
|
||||
this.glowMesh = null;
|
||||
this.panelInstances = null;
|
||||
}
|
||||
|
||||
private animate = (): void => {
|
||||
this.animId = requestAnimationFrame(this.animate);
|
||||
this.time += 0.005 * this.speedMultiplier;
|
||||
|
||||
if (this.shellMesh) this.shellMesh.rotation.y = this.time * 0.3;
|
||||
if (this.panelInstances) this.panelInstances.rotation.y = this.time * 0.3;
|
||||
|
||||
// Auto-rotate camera
|
||||
if (this.autoRotate) {
|
||||
const camR = 4;
|
||||
this.camera.position.x = camR * Math.sin(this.time * 0.15);
|
||||
this.camera.position.z = camR * Math.cos(this.time * 0.15);
|
||||
this.camera.position.y = 1.2 + 0.3 * Math.sin(this.time * 0.1);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
}
|
||||
|
||||
if (this.starMesh) {
|
||||
const scale = 1 + 0.03 * Math.sin(this.time * 3);
|
||||
this.starMesh.scale.setScalar(scale);
|
||||
}
|
||||
|
||||
if (this.glowMesh) {
|
||||
const mat = this.glowMesh.material as THREE.MeshBasicMaterial;
|
||||
mat.opacity = 0.04 + 0.02 * Math.sin(this.time * 2);
|
||||
}
|
||||
|
||||
this.controls.update();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
};
|
||||
|
||||
resize(): void {
|
||||
const w = this.container.clientWidth || 400;
|
||||
const h = this.container.clientHeight || 300;
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(w, h);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
cancelAnimationFrame(this.animId);
|
||||
this.controls.dispose();
|
||||
this.clearSystem();
|
||||
if (this.bgGroup) {
|
||||
this.scene.remove(this.bgGroup);
|
||||
this.bgGroup.traverse((obj) => {
|
||||
if ((obj as THREE.Mesh).geometry) (obj as THREE.Mesh).geometry.dispose();
|
||||
});
|
||||
this.bgGroup = null;
|
||||
}
|
||||
this.renderer.dispose();
|
||||
if (this.renderer.domElement.parentElement) {
|
||||
this.renderer.domElement.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
204
vendor/ruvector/examples/rvf/dashboard/src/three/OrbitPreview.ts
vendored
Normal file
204
vendor/ruvector/examples/rvf/dashboard/src/three/OrbitPreview.ts
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class OrbitPreview {
|
||||
private line: THREE.Line | null = null;
|
||||
private starMesh: THREE.Mesh | null = null;
|
||||
private starGlow: THREE.Sprite | null = null;
|
||||
private planetMesh: THREE.Mesh | null = null;
|
||||
private hzRing: THREE.Line | null = null;
|
||||
private gridHelper: THREE.GridHelper | null = null;
|
||||
private scene: THREE.Scene;
|
||||
private orbitPoints: THREE.Vector3[] = [];
|
||||
private orbitAngle = 0;
|
||||
private orbitSpeed = 0.005;
|
||||
private paramOverlay: HTMLElement | null = null;
|
||||
private parentEl: HTMLElement | null = null;
|
||||
|
||||
constructor(scene: THREE.Scene) {
|
||||
this.scene = scene;
|
||||
this.addStar();
|
||||
this.addGrid();
|
||||
}
|
||||
|
||||
private addStar(): void {
|
||||
// Solid sphere
|
||||
const geo = new THREE.SphereGeometry(0.18, 24, 16);
|
||||
const mat = new THREE.MeshBasicMaterial({ color: 0xffdd44 });
|
||||
this.starMesh = new THREE.Mesh(geo, mat);
|
||||
this.scene.add(this.starMesh);
|
||||
|
||||
// Glow sprite
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
const grad = ctx.createRadialGradient(32, 32, 2, 32, 32, 32);
|
||||
grad.addColorStop(0, 'rgba(255,221,68,0.6)');
|
||||
grad.addColorStop(0.4, 'rgba(255,200,50,0.15)');
|
||||
grad.addColorStop(1, 'rgba(255,200,50,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 64, 64);
|
||||
}
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true, blending: THREE.AdditiveBlending });
|
||||
this.starGlow = new THREE.Sprite(spriteMat);
|
||||
this.starGlow.scale.set(1.2, 1.2, 1);
|
||||
this.scene.add(this.starGlow);
|
||||
}
|
||||
|
||||
private addGrid(): void {
|
||||
this.gridHelper = new THREE.GridHelper(8, 8, 0x1C2333, 0x151B23);
|
||||
this.gridHelper.position.y = -0.5;
|
||||
this.scene.add(this.gridHelper);
|
||||
}
|
||||
|
||||
setOrbit(
|
||||
semiMajorAxis: number,
|
||||
eccentricity: number,
|
||||
inclination: number,
|
||||
parentElement?: HTMLElement,
|
||||
): void {
|
||||
this.disposeLine();
|
||||
this.disposePlanet();
|
||||
this.disposeHzRing();
|
||||
this.disposeOverlay();
|
||||
|
||||
const segments = 128;
|
||||
this.orbitPoints = [];
|
||||
const a = semiMajorAxis;
|
||||
const e = Math.min(Math.max(eccentricity, 0), 0.99);
|
||||
const incRad = (inclination * Math.PI) / 180;
|
||||
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
const theta = (i / segments) * Math.PI * 2;
|
||||
const r = (a * (1 - e * e)) / (1 + e * Math.cos(theta));
|
||||
const x = r * Math.cos(theta);
|
||||
const z = r * Math.sin(theta) * Math.cos(incRad);
|
||||
const y = r * Math.sin(theta) * Math.sin(incRad);
|
||||
this.orbitPoints.push(new THREE.Vector3(x, y, z));
|
||||
}
|
||||
|
||||
// Orbit path
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(this.orbitPoints);
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: 0x4488ff,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
});
|
||||
this.line = new THREE.Line(geometry, material);
|
||||
this.scene.add(this.line);
|
||||
|
||||
// Planet dot
|
||||
const planetGeo = new THREE.SphereGeometry(0.08, 12, 8);
|
||||
const planetMat = new THREE.MeshStandardMaterial({ color: 0x4488ff, emissive: 0x2244aa, emissiveIntensity: 0.3 });
|
||||
this.planetMesh = new THREE.Mesh(planetGeo, planetMat);
|
||||
this.planetMesh.position.copy(this.orbitPoints[0]);
|
||||
this.scene.add(this.planetMesh);
|
||||
|
||||
// Habitable zone ring (0.95-1.37 AU scaled)
|
||||
const hzInner = 0.95 * (a / 1.5);
|
||||
const hzOuter = 1.37 * (a / 1.5);
|
||||
const hzMid = (hzInner + hzOuter) / 2;
|
||||
const hzPts: THREE.Vector3[] = [];
|
||||
for (let i = 0; i <= 64; i++) {
|
||||
const theta = (i / 64) * Math.PI * 2;
|
||||
hzPts.push(new THREE.Vector3(hzMid * Math.cos(theta), -0.48, hzMid * Math.sin(theta)));
|
||||
}
|
||||
const hzGeo = new THREE.BufferGeometry().setFromPoints(hzPts);
|
||||
const hzMat = new THREE.LineBasicMaterial({ color: 0x2ECC71, transparent: true, opacity: 0.25 });
|
||||
this.hzRing = new THREE.Line(hzGeo, hzMat);
|
||||
this.scene.add(this.hzRing);
|
||||
|
||||
// Orbit speed based on period (faster for shorter periods)
|
||||
this.orbitSpeed = 0.003 + (1 / (a * 10)) * 0.02;
|
||||
this.orbitAngle = 0;
|
||||
|
||||
// Param overlay
|
||||
if (parentElement) {
|
||||
this.parentEl = parentElement;
|
||||
this.paramOverlay = document.createElement('div');
|
||||
this.paramOverlay.style.cssText =
|
||||
'position:absolute;bottom:8px;left:8px;' +
|
||||
'background:rgba(11,15,20,0.85);border:1px solid var(--border);border-radius:4px;' +
|
||||
'padding:6px 10px;font-family:var(--font-mono);font-size:10px;color:var(--text-secondary);' +
|
||||
'line-height:1.6;z-index:10;pointer-events:none';
|
||||
this.paramOverlay.innerHTML =
|
||||
`<div style="color:var(--text-primary);font-weight:600;margin-bottom:2px">Orbit Parameters</div>` +
|
||||
`<div>Semi-major: <span style="color:var(--accent)">${a.toFixed(2)} AU</span></div>` +
|
||||
`<div>Eccentricity: <span style="color:var(--accent)">${e.toFixed(3)}</span></div>` +
|
||||
`<div>Inclination: <span style="color:var(--accent)">${inclination.toFixed(1)}°</span></div>` +
|
||||
`<div style="margin-top:4px;color:#2ECC71;font-size:9px">● Habitable zone</div>`;
|
||||
parentElement.appendChild(this.paramOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
/** Call each frame to animate the planet along the orbit. */
|
||||
tick(): void {
|
||||
if (!this.planetMesh || this.orbitPoints.length < 2) return;
|
||||
this.orbitAngle = (this.orbitAngle + this.orbitSpeed) % 1;
|
||||
const idx = Math.floor(this.orbitAngle * (this.orbitPoints.length - 1));
|
||||
this.planetMesh.position.copy(this.orbitPoints[idx]);
|
||||
}
|
||||
|
||||
private disposeLine(): void {
|
||||
if (this.line) {
|
||||
this.scene.remove(this.line);
|
||||
this.line.geometry.dispose();
|
||||
(this.line.material as THREE.Material).dispose();
|
||||
this.line = null;
|
||||
}
|
||||
}
|
||||
|
||||
private disposePlanet(): void {
|
||||
if (this.planetMesh) {
|
||||
this.scene.remove(this.planetMesh);
|
||||
this.planetMesh.geometry.dispose();
|
||||
(this.planetMesh.material as THREE.Material).dispose();
|
||||
this.planetMesh = null;
|
||||
}
|
||||
}
|
||||
|
||||
private disposeHzRing(): void {
|
||||
if (this.hzRing) {
|
||||
this.scene.remove(this.hzRing);
|
||||
this.hzRing.geometry.dispose();
|
||||
(this.hzRing.material as THREE.Material).dispose();
|
||||
this.hzRing = null;
|
||||
}
|
||||
}
|
||||
|
||||
private disposeOverlay(): void {
|
||||
if (this.paramOverlay && this.parentEl) {
|
||||
this.parentEl.removeChild(this.paramOverlay);
|
||||
this.paramOverlay = null;
|
||||
this.parentEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposeLine();
|
||||
this.disposePlanet();
|
||||
this.disposeHzRing();
|
||||
this.disposeOverlay();
|
||||
|
||||
if (this.starMesh) {
|
||||
this.scene.remove(this.starMesh);
|
||||
this.starMesh.geometry.dispose();
|
||||
(this.starMesh.material as THREE.Material).dispose();
|
||||
this.starMesh = null;
|
||||
}
|
||||
if (this.starGlow) {
|
||||
this.scene.remove(this.starGlow);
|
||||
this.starGlow.material.map?.dispose();
|
||||
this.starGlow.material.dispose();
|
||||
this.starGlow = null;
|
||||
}
|
||||
if (this.gridHelper) {
|
||||
this.scene.remove(this.gridHelper);
|
||||
this.gridHelper.geometry.dispose();
|
||||
(this.gridHelper.material as THREE.Material).dispose();
|
||||
this.gridHelper = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
544
vendor/ruvector/examples/rvf/dashboard/src/three/PlanetSystem3D.ts
vendored
Normal file
544
vendor/ruvector/examples/rvf/dashboard/src/three/PlanetSystem3D.ts
vendored
Normal file
@@ -0,0 +1,544 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
/**
|
||||
* Interactive 3D exoplanet system visualization with galactic context.
|
||||
*
|
||||
* Renders:
|
||||
* - Deep-field starfield (4000 background stars)
|
||||
* - Milky Way galactic plane disc
|
||||
* - Distant nebula patches
|
||||
* - Central host star (color from effective temperature)
|
||||
* - Planet on animated orbital path (size from radius_earth)
|
||||
* - Habitable zone annulus (green band)
|
||||
* - Orbit ellipse line
|
||||
* - AU scale labels
|
||||
*
|
||||
* Interaction:
|
||||
* - OrbitControls: drag to rotate, scroll to zoom, right-drag to pan
|
||||
* - Speed control via setSpeed()
|
||||
* - Reset view via resetCamera()
|
||||
*/
|
||||
|
||||
export interface PlanetSystemParams {
|
||||
label: string;
|
||||
radiusEarth: number;
|
||||
semiMajorAxisAU: number;
|
||||
eqTempK: number;
|
||||
stellarTempK: number;
|
||||
stellarRadiusSolar: number;
|
||||
periodDays: number;
|
||||
hzMember: boolean;
|
||||
esiScore: number;
|
||||
transitDepth: number;
|
||||
}
|
||||
|
||||
function starColorFromTemp(teff: number): number {
|
||||
if (teff > 7500) return 0xaabfff;
|
||||
if (teff > 6000) return 0xf8f7ff;
|
||||
if (teff > 5200) return 0xfff4ea;
|
||||
if (teff > 3700) return 0xffd2a1;
|
||||
return 0xffb56c;
|
||||
}
|
||||
|
||||
function planetColor(eqTempK: number): number {
|
||||
if (eqTempK < 200) return 0x4488cc;
|
||||
if (eqTempK < 260) return 0x44aa77;
|
||||
if (eqTempK < 320) return 0x55bb55;
|
||||
if (eqTempK < 500) return 0xddaa44;
|
||||
return 0xff6644;
|
||||
}
|
||||
|
||||
/** Deterministic pseudo-random from seed. */
|
||||
function seededRandom(seed: number): () => number {
|
||||
let s = seed;
|
||||
return () => {
|
||||
s = (s * 16807 + 0) % 2147483647;
|
||||
return (s - 1) / 2147483646;
|
||||
};
|
||||
}
|
||||
|
||||
export class PlanetSystem3D {
|
||||
private scene: THREE.Scene;
|
||||
private camera: THREE.PerspectiveCamera;
|
||||
private renderer: THREE.WebGLRenderer;
|
||||
private controls: OrbitControls;
|
||||
private starMesh: THREE.Mesh | null = null;
|
||||
private planetMesh: THREE.Mesh | null = null;
|
||||
private orbitLine: THREE.Line | null = null;
|
||||
private hzInnerRing: THREE.Mesh | null = null;
|
||||
private animId = 0;
|
||||
private time = 0;
|
||||
private orbitPoints: THREE.Vector3[] = [];
|
||||
private orbitSpeed = 0.003;
|
||||
private orbitAngle = 0;
|
||||
private speedMultiplier = 1;
|
||||
private autoRotate = true;
|
||||
private defaultCamPos = new THREE.Vector3(0, 3, 6);
|
||||
private bgGroup: THREE.Group | null = null;
|
||||
private labelSprites: THREE.Sprite[] = [];
|
||||
private currentParams: PlanetSystemParams | null = null;
|
||||
|
||||
constructor(private container: HTMLElement) {
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x020408);
|
||||
|
||||
const w = container.clientWidth || 400;
|
||||
const h = container.clientHeight || 300;
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(50, w / h, 0.01, 2000);
|
||||
this.camera.position.set(0, 3, 6);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
this.renderer.setSize(w, h);
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
container.appendChild(this.renderer.domElement);
|
||||
|
||||
// OrbitControls for mouse interaction
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.08;
|
||||
this.controls.minDistance = 1;
|
||||
this.controls.maxDistance = 500;
|
||||
this.controls.enablePan = true;
|
||||
this.controls.autoRotate = false; // We handle auto-rotate ourselves
|
||||
this.controls.zoomSpeed = 1.2;
|
||||
this.controls.rotateSpeed = 0.8;
|
||||
|
||||
// Stop auto-rotation when user interacts
|
||||
this.controls.addEventListener('start', () => { this.autoRotate = false; });
|
||||
|
||||
this.scene.add(new THREE.AmbientLight(0x222244, 0.4));
|
||||
|
||||
// Build immutable background (stars, galaxy, nebulae)
|
||||
this.buildBackground();
|
||||
}
|
||||
|
||||
// ── Public controls ──
|
||||
|
||||
setSpeed(multiplier: number): void {
|
||||
this.speedMultiplier = multiplier;
|
||||
}
|
||||
|
||||
resetCamera(): void {
|
||||
this.autoRotate = true;
|
||||
this.camera.position.copy(this.defaultCamPos);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
toggleAutoRotate(): void {
|
||||
this.autoRotate = !this.autoRotate;
|
||||
}
|
||||
|
||||
getAutoRotate(): boolean {
|
||||
return this.autoRotate;
|
||||
}
|
||||
|
||||
// ── Background: starfield, galaxy, nebulae ──
|
||||
|
||||
private buildBackground(): void {
|
||||
this.bgGroup = new THREE.Group();
|
||||
|
||||
// ── Starfield: 4000 background stars ──
|
||||
const starCount = 4000;
|
||||
const positions = new Float32Array(starCount * 3);
|
||||
const colors = new Float32Array(starCount * 3);
|
||||
const sizes = new Float32Array(starCount);
|
||||
const rand = seededRandom(42);
|
||||
|
||||
const starTints = [
|
||||
new THREE.Color(0xffffff), // white
|
||||
new THREE.Color(0xaaccff), // blue-white
|
||||
new THREE.Color(0xfff4ea), // yellow-white
|
||||
new THREE.Color(0xffd2a1), // orange
|
||||
new THREE.Color(0xffb56c), // red-orange
|
||||
new THREE.Color(0xccddff), // pale blue
|
||||
];
|
||||
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
// Distribute on a large sphere shell (300-800 units away)
|
||||
const theta = rand() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * rand() - 1);
|
||||
const r = 300 + rand() * 500;
|
||||
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
||||
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
||||
positions[i * 3 + 2] = r * Math.cos(phi);
|
||||
|
||||
const tint = starTints[Math.floor(rand() * starTints.length)];
|
||||
const brightness = 0.4 + rand() * 0.6;
|
||||
colors[i * 3] = tint.r * brightness;
|
||||
colors[i * 3 + 1] = tint.g * brightness;
|
||||
colors[i * 3 + 2] = tint.b * brightness;
|
||||
|
||||
sizes[i] = 0.5 + rand() * 2.0;
|
||||
}
|
||||
|
||||
const starGeo = new THREE.BufferGeometry();
|
||||
starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
starGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
starGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
||||
|
||||
const starMat = new THREE.PointsMaterial({
|
||||
size: 1.5,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
sizeAttenuation: true,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.bgGroup.add(new THREE.Points(starGeo, starMat));
|
||||
|
||||
// ── Milky Way galactic plane ──
|
||||
// A large tilted disc with dense star concentration
|
||||
const galaxyCount = 6000;
|
||||
const galPos = new Float32Array(galaxyCount * 3);
|
||||
const galCol = new Float32Array(galaxyCount * 3);
|
||||
for (let i = 0; i < galaxyCount; i++) {
|
||||
// Flat disc distribution, concentrated toward center
|
||||
const angle = rand() * Math.PI * 2;
|
||||
const dist = Math.pow(rand(), 0.5) * 600; // More concentrated near center
|
||||
const height = (rand() - 0.5) * (15 + dist * 0.02); // Thin disc, thicker outward
|
||||
|
||||
galPos[i * 3] = dist * Math.cos(angle);
|
||||
galPos[i * 3 + 1] = height;
|
||||
galPos[i * 3 + 2] = dist * Math.sin(angle);
|
||||
|
||||
// Milky Way is blueish-white with warm core
|
||||
const coreProx = 1 - Math.min(1, dist / 600);
|
||||
const r2 = rand();
|
||||
galCol[i * 3] = 0.5 + coreProx * 0.4 + r2 * 0.1;
|
||||
galCol[i * 3 + 1] = 0.5 + coreProx * 0.3 + r2 * 0.1;
|
||||
galCol[i * 3 + 2] = 0.6 + r2 * 0.15;
|
||||
}
|
||||
|
||||
const galGeo = new THREE.BufferGeometry();
|
||||
galGeo.setAttribute('position', new THREE.BufferAttribute(galPos, 3));
|
||||
galGeo.setAttribute('color', new THREE.BufferAttribute(galCol, 3));
|
||||
const galMat = new THREE.PointsMaterial({
|
||||
size: 0.8,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.25,
|
||||
sizeAttenuation: true,
|
||||
depthWrite: false,
|
||||
});
|
||||
const galaxy = new THREE.Points(galGeo, galMat);
|
||||
// Tilt the galactic plane ~60 degrees (we see the Milky Way at an angle)
|
||||
galaxy.rotation.x = Math.PI * 0.35;
|
||||
galaxy.rotation.z = Math.PI * 0.15;
|
||||
galaxy.position.set(0, 100, -200);
|
||||
this.bgGroup.add(galaxy);
|
||||
|
||||
// ── Galactic core glow ──
|
||||
const coreGlowGeo = new THREE.SphereGeometry(40, 16, 16);
|
||||
const coreGlowMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xeeddcc,
|
||||
transparent: true,
|
||||
opacity: 0.04,
|
||||
side: THREE.BackSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
const coreGlow = new THREE.Mesh(coreGlowGeo, coreGlowMat);
|
||||
coreGlow.position.copy(galaxy.position);
|
||||
this.bgGroup.add(coreGlow);
|
||||
|
||||
// ── Nebula patches (colored sprite billboards) ──
|
||||
const nebulaColors = [0x3344aa, 0xaa3355, 0x2288aa, 0x8844aa, 0x44aa66];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const grad = ctx.createRadialGradient(64, 64, 4, 64, 64, 64);
|
||||
const col = new THREE.Color(nebulaColors[i % nebulaColors.length]);
|
||||
grad.addColorStop(0, `rgba(${Math.floor(col.r * 255)},${Math.floor(col.g * 255)},${Math.floor(col.b * 255)},0.3)`);
|
||||
grad.addColorStop(0.4, `rgba(${Math.floor(col.r * 255)},${Math.floor(col.g * 255)},${Math.floor(col.b * 255)},0.08)`);
|
||||
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 128, 128);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const spriteMat = new THREE.SpriteMaterial({
|
||||
map: tex,
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
});
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
const theta2 = rand() * Math.PI * 2;
|
||||
const phi2 = (rand() - 0.5) * Math.PI * 0.6;
|
||||
const r2 = 200 + rand() * 400;
|
||||
sprite.position.set(
|
||||
r2 * Math.cos(phi2) * Math.cos(theta2),
|
||||
r2 * Math.sin(phi2),
|
||||
r2 * Math.cos(phi2) * Math.sin(theta2),
|
||||
);
|
||||
sprite.scale.setScalar(60 + rand() * 120);
|
||||
this.bgGroup.add(sprite);
|
||||
}
|
||||
|
||||
this.scene.add(this.bgGroup);
|
||||
}
|
||||
|
||||
update(params: PlanetSystemParams): void {
|
||||
this.clearSystem();
|
||||
this.currentParams = params;
|
||||
|
||||
const sc = starColorFromTemp(params.stellarTempK);
|
||||
const pc = planetColor(params.eqTempK);
|
||||
const orbitRadius = Math.max(0.8, Math.min(4.0, params.semiMajorAxisAU * 2.5));
|
||||
|
||||
// ── Host Star ──
|
||||
const starVisualRadius = 0.25 + params.stellarRadiusSolar * 0.2;
|
||||
const starGeo = new THREE.SphereGeometry(starVisualRadius, 32, 32);
|
||||
const starMat = new THREE.MeshBasicMaterial({ color: sc });
|
||||
this.starMesh = new THREE.Mesh(starGeo, starMat);
|
||||
this.scene.add(this.starMesh);
|
||||
|
||||
const starLight = new THREE.PointLight(sc, 2.5, 30);
|
||||
starLight.position.set(0, 0, 0);
|
||||
this.scene.add(starLight);
|
||||
|
||||
// Star corona
|
||||
const glowGeo = new THREE.SphereGeometry(starVisualRadius * 2.5, 24, 24);
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: sc,
|
||||
transparent: true,
|
||||
opacity: 0.05,
|
||||
side: THREE.BackSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.scene.add(new THREE.Mesh(glowGeo, glowMat));
|
||||
|
||||
// ── Habitable Zone ──
|
||||
if (params.hzMember) {
|
||||
const hzInner = orbitRadius * 0.75;
|
||||
const hzOuter = orbitRadius * 1.35;
|
||||
const hzGeo = new THREE.RingGeometry(hzInner, hzOuter, 64);
|
||||
const hzMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x2ecc71,
|
||||
transparent: true,
|
||||
opacity: 0.07,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.hzInnerRing = new THREE.Mesh(hzGeo, hzMat);
|
||||
this.hzInnerRing.rotation.x = -Math.PI / 2;
|
||||
this.hzInnerRing.position.y = -0.01;
|
||||
this.scene.add(this.hzInnerRing);
|
||||
|
||||
const makeHzCircle = (r: number, color: number, opacity: number) => {
|
||||
const pts: THREE.Vector3[] = [];
|
||||
for (let i = 0; i <= 128; i++) {
|
||||
const th = (i / 128) * Math.PI * 2;
|
||||
pts.push(new THREE.Vector3(r * Math.cos(th), 0, r * Math.sin(th)));
|
||||
}
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(pts);
|
||||
const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity });
|
||||
this.scene.add(new THREE.Line(geo, mat));
|
||||
};
|
||||
makeHzCircle(hzInner, 0x2ecc71, 0.25);
|
||||
makeHzCircle(hzOuter, 0x2ecc71, 0.12);
|
||||
}
|
||||
|
||||
// ── Orbit Path ──
|
||||
this.orbitPoints = [];
|
||||
const segments = 256;
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
const theta = (i / segments) * Math.PI * 2;
|
||||
this.orbitPoints.push(new THREE.Vector3(
|
||||
orbitRadius * Math.cos(theta), 0, orbitRadius * Math.sin(theta),
|
||||
));
|
||||
}
|
||||
const orbitGeo = new THREE.BufferGeometry().setFromPoints(this.orbitPoints);
|
||||
const orbitMat = new THREE.LineBasicMaterial({ color: pc, transparent: true, opacity: 0.5 });
|
||||
this.orbitLine = new THREE.Line(orbitGeo, orbitMat);
|
||||
this.scene.add(this.orbitLine);
|
||||
|
||||
// Reference rings
|
||||
const makeRefRing = (r: number) => {
|
||||
const pts: THREE.Vector3[] = [];
|
||||
for (let i = 0; i <= 128; i++) {
|
||||
const th = (i / 128) * Math.PI * 2;
|
||||
pts.push(new THREE.Vector3(r * Math.cos(th), 0, r * Math.sin(th)));
|
||||
}
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(pts);
|
||||
const mat = new THREE.LineBasicMaterial({ color: 0x1c2333, transparent: true, opacity: 0.3 });
|
||||
this.scene.add(new THREE.Line(geo, mat));
|
||||
};
|
||||
makeRefRing(0.5 * 2.5);
|
||||
makeRefRing(1.5 * 2.5);
|
||||
|
||||
// AU scale labels
|
||||
this.addScaleLabel('0.5 AU', 0.5 * 2.5 + 0.2, 0.3, 0);
|
||||
this.addScaleLabel('1.0 AU', 1.0 * 2.5 + 0.2, 0.3, 0);
|
||||
this.addScaleLabel('1.5 AU', 1.5 * 2.5 + 0.2, 0.3, 0);
|
||||
if (params.hzMember) {
|
||||
this.addScaleLabel('HZ', orbitRadius * 1.05, 0.5, 0, '#2ecc71');
|
||||
}
|
||||
|
||||
// ── Planet ──
|
||||
const planetVisualRadius = Math.max(0.06, Math.min(0.2, params.radiusEarth * 0.1));
|
||||
const planetGeo = new THREE.SphereGeometry(planetVisualRadius, 24, 24);
|
||||
const planetMat = new THREE.MeshStandardMaterial({
|
||||
color: pc,
|
||||
emissive: pc,
|
||||
emissiveIntensity: 0.2,
|
||||
roughness: 0.7,
|
||||
metalness: 0.1,
|
||||
});
|
||||
this.planetMesh = new THREE.Mesh(planetGeo, planetMat);
|
||||
this.planetMesh.position.copy(this.orbitPoints[0]);
|
||||
this.scene.add(this.planetMesh);
|
||||
|
||||
// Atmosphere halo for habitable candidates
|
||||
if (params.hzMember && params.eqTempK > 180 && params.eqTempK < 350) {
|
||||
const atmoGeo = new THREE.SphereGeometry(planetVisualRadius * 1.2, 24, 24);
|
||||
const atmoMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x66ccff,
|
||||
transparent: true,
|
||||
opacity: 0.12,
|
||||
side: THREE.BackSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.planetMesh.add(new THREE.Mesh(atmoGeo, atmoMat));
|
||||
}
|
||||
|
||||
// Planet label
|
||||
this.addScaleLabel(
|
||||
params.label,
|
||||
this.orbitPoints[0].x,
|
||||
this.orbitPoints[0].y + planetVisualRadius + 0.15,
|
||||
this.orbitPoints[0].z,
|
||||
'#00e5ff',
|
||||
);
|
||||
|
||||
// ── Grid ──
|
||||
const gridHelper = new THREE.GridHelper(12, 12, 0x151b23, 0x0d1117);
|
||||
gridHelper.position.y = -0.3;
|
||||
this.scene.add(gridHelper);
|
||||
|
||||
// Speed and camera
|
||||
this.orbitSpeed = 0.002 + (1 / Math.max(params.periodDays, 10)) * 0.8;
|
||||
this.orbitAngle = 0;
|
||||
|
||||
const camDist = orbitRadius * 1.8 + 2;
|
||||
this.defaultCamPos.set(camDist * 0.6, camDist * 0.45, camDist * 0.7);
|
||||
this.camera.position.copy(this.defaultCamPos);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
this.controls.update();
|
||||
this.autoRotate = true;
|
||||
|
||||
this.animate();
|
||||
}
|
||||
|
||||
private addScaleLabel(text: string, x: number, y: number, z: number, color = '#556677'): void {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128;
|
||||
canvas.height = 32;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.font = '14px monospace';
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(text, 64, 20);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
tex.minFilter = THREE.LinearFilter;
|
||||
const mat = new THREE.SpriteMaterial({
|
||||
map: tex,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
});
|
||||
const sprite = new THREE.Sprite(mat);
|
||||
sprite.position.set(x, y, z);
|
||||
sprite.scale.set(1.2, 0.3, 1);
|
||||
this.scene.add(sprite);
|
||||
this.labelSprites.push(sprite);
|
||||
}
|
||||
|
||||
/** Remove system objects but keep background. */
|
||||
private clearSystem(): void {
|
||||
cancelAnimationFrame(this.animId);
|
||||
const toRemove: THREE.Object3D[] = [];
|
||||
this.scene.traverse((obj) => {
|
||||
if (obj !== this.scene && obj !== this.bgGroup && obj.parent === this.scene && !(obj instanceof THREE.AmbientLight)) {
|
||||
toRemove.push(obj);
|
||||
}
|
||||
});
|
||||
for (const obj of toRemove) {
|
||||
this.scene.remove(obj);
|
||||
if ((obj as THREE.Mesh).geometry) (obj as THREE.Mesh).geometry.dispose();
|
||||
}
|
||||
// Re-add ambient if missing
|
||||
let hasAmbient = false;
|
||||
this.scene.traverse((o) => { if (o instanceof THREE.AmbientLight) hasAmbient = true; });
|
||||
if (!hasAmbient) this.scene.add(new THREE.AmbientLight(0x222244, 0.4));
|
||||
|
||||
this.starMesh = null;
|
||||
this.planetMesh = null;
|
||||
this.orbitLine = null;
|
||||
this.hzInnerRing = null;
|
||||
this.labelSprites = [];
|
||||
}
|
||||
|
||||
private animate = (): void => {
|
||||
this.animId = requestAnimationFrame(this.animate);
|
||||
this.time += 0.005 * this.speedMultiplier;
|
||||
|
||||
// Planet orbit
|
||||
if (this.planetMesh && this.orbitPoints.length > 1) {
|
||||
this.orbitAngle = (this.orbitAngle + this.orbitSpeed * this.speedMultiplier) % 1;
|
||||
const idx = Math.floor(this.orbitAngle * (this.orbitPoints.length - 1));
|
||||
this.planetMesh.position.copy(this.orbitPoints[idx]);
|
||||
this.planetMesh.rotation.y += 0.01 * this.speedMultiplier;
|
||||
}
|
||||
|
||||
// Star pulse
|
||||
if (this.starMesh) {
|
||||
const scale = 1 + 0.02 * Math.sin(this.time * 3);
|
||||
this.starMesh.scale.setScalar(scale);
|
||||
}
|
||||
|
||||
// Auto-rotate camera (only if user hasn't grabbed controls)
|
||||
if (this.autoRotate) {
|
||||
const camDist = this.camera.position.length();
|
||||
this.camera.position.x = camDist * 0.7 * Math.sin(this.time * 0.1);
|
||||
this.camera.position.z = camDist * 0.7 * Math.cos(this.time * 0.1);
|
||||
this.camera.position.y = camDist * 0.35 + 0.5 * Math.sin(this.time * 0.07);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
}
|
||||
|
||||
this.controls.update();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
};
|
||||
|
||||
resize(): void {
|
||||
const w = this.container.clientWidth || 400;
|
||||
const h = this.container.clientHeight || 300;
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(w, h);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
cancelAnimationFrame(this.animId);
|
||||
this.controls.dispose();
|
||||
this.clearSystem();
|
||||
// Also clear background
|
||||
if (this.bgGroup) {
|
||||
this.scene.remove(this.bgGroup);
|
||||
this.bgGroup.traverse((obj) => {
|
||||
if ((obj as THREE.Mesh).geometry) (obj as THREE.Mesh).geometry.dispose();
|
||||
});
|
||||
this.bgGroup = null;
|
||||
}
|
||||
this.renderer.dispose();
|
||||
if (this.renderer.domElement.parentElement) {
|
||||
this.renderer.domElement.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
701
vendor/ruvector/examples/rvf/dashboard/src/views/AtlasExplorer.ts
vendored
Normal file
701
vendor/ruvector/examples/rvf/dashboard/src/views/AtlasExplorer.ts
vendored
Normal file
@@ -0,0 +1,701 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { AtlasGraph, GraphNode, GraphEdge } from '../three/AtlasGraph';
|
||||
import { fetchAtlasQuery } from '../api';
|
||||
import { onEvent, LiveEvent } from '../ws';
|
||||
|
||||
const SCALES = ['2h', '12h', '3d', '27d'] as const;
|
||||
|
||||
/* ── Seeded RNG ── */
|
||||
function seededRng(seed: number): () => number {
|
||||
let s = seed | 0;
|
||||
return () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
|
||||
}
|
||||
|
||||
/* ── Constellation name generator ── */
|
||||
const CONSTELLATION_NAMES = [
|
||||
'Lyra', 'Cygnus', 'Aquila', 'Orion', 'Centaurus', 'Vela', 'Puppis',
|
||||
'Sagittarius', 'Scorpius', 'Cassiopeia', 'Perseus', 'Andromeda',
|
||||
'Draco', 'Ursa Major', 'Leo', 'Virgo', 'Libra', 'Gemini',
|
||||
];
|
||||
|
||||
/* ── Galaxy data generator ── */
|
||||
function generateGalaxyData(
|
||||
scale: string,
|
||||
nodeCount: number,
|
||||
armCount: number,
|
||||
armSpread: number,
|
||||
coreConcentration: number,
|
||||
): { nodes: GraphNode[]; edges: GraphEdge[] } {
|
||||
const n = nodeCount;
|
||||
const domains = ['transit', 'flare', 'rotation', 'eclipse', 'variability'];
|
||||
const nodes: GraphNode[] = [];
|
||||
const edges: GraphEdge[] = [];
|
||||
const rng = seededRng(scale.length * 31337);
|
||||
|
||||
const maxRadius = 8;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const arm = i % armCount;
|
||||
const armAngle = (arm / armCount) * Math.PI * 2;
|
||||
const t = rng();
|
||||
// Core concentration: power law pushes more nodes toward center
|
||||
const radius = 0.3 + Math.pow(t, coreConcentration) * maxRadius;
|
||||
|
||||
const spiralAngle = armAngle + radius * 0.6 + (rng() - 0.5) * armSpread;
|
||||
const diskHeight = (rng() - 0.5) * 0.5 * Math.exp(-radius * 0.12);
|
||||
|
||||
const scatter = radius * 0.08;
|
||||
const x = radius * Math.cos(spiralAngle) + (rng() - 0.5) * scatter;
|
||||
const z = radius * Math.sin(spiralAngle) + (rng() - 0.5) * scatter;
|
||||
const y = diskHeight;
|
||||
|
||||
const weight = 0.15 + rng() * 0.85;
|
||||
nodes.push({ id: `s${i}`, domain: domains[i % domains.length], x, y, z, weight });
|
||||
}
|
||||
|
||||
// Edges: connect nearby stars
|
||||
for (let i = 1; i < n; i++) {
|
||||
let bestDist = Infinity;
|
||||
let bestJ = 0;
|
||||
const searchRange = Math.min(i, 25);
|
||||
for (let j = Math.max(0, i - searchRange); j < i; j++) {
|
||||
const dx = nodes[i].x - nodes[j].x;
|
||||
const dy = nodes[i].y - nodes[j].y;
|
||||
const dz = nodes[i].z - nodes[j].z;
|
||||
const d = dx * dx + dy * dy + dz * dz;
|
||||
if (d < bestDist) { bestDist = d; bestJ = j; }
|
||||
}
|
||||
edges.push({ source: nodes[bestJ].id, target: nodes[i].id, weight: Math.max(0.1, 1 - bestDist / 16) });
|
||||
|
||||
// Cross-arm connections
|
||||
if (rng() > 0.85 && i > 4) {
|
||||
const extra = Math.floor(rng() * i);
|
||||
const dx = nodes[i].x - nodes[extra].x;
|
||||
const dz = nodes[i].z - nodes[extra].z;
|
||||
if (Math.sqrt(dx * dx + dz * dz) < 4) {
|
||||
edges.push({ source: nodes[extra].id, target: nodes[i].id, weight: 0.05 + rng() * 0.15 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
export class AtlasExplorer {
|
||||
private container: HTMLElement | null = null;
|
||||
private renderer: THREE.WebGLRenderer | null = null;
|
||||
private scene: THREE.Scene | null = null;
|
||||
private camera: THREE.PerspectiveCamera | null = null;
|
||||
private controls: OrbitControls | null = null;
|
||||
private graph: AtlasGraph | null = null;
|
||||
private starfield: THREE.Points | null = null;
|
||||
private nebulaGroup: THREE.Group | null = null;
|
||||
private starMapLabels: THREE.Group | null = null;
|
||||
private gridHelper: THREE.Group | null = null;
|
||||
private animFrameId = 0;
|
||||
private unsubWs: (() => void) | null = null;
|
||||
private activeScale: string = '12h';
|
||||
private time = 0;
|
||||
|
||||
// Configurable parameters
|
||||
private nodeCount = 150;
|
||||
private spiralArms = 4;
|
||||
private armSpread = 0.4;
|
||||
private coreConcentration = 1.0;
|
||||
private rotationSpeed = 0.15;
|
||||
private showGrid = true;
|
||||
private showLabels = true;
|
||||
private showEdges = true;
|
||||
private pulseNodes = true;
|
||||
|
||||
// DOM refs for live slider updates
|
||||
private sliderRefs: Map<string, { slider: HTMLInputElement; valEl: HTMLElement }> = new Map();
|
||||
private statsEl: HTMLElement | null = null;
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'display:flex;width:100%;height:100%;overflow:hidden';
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// Left sidebar: config + star map info
|
||||
const sidebar = this.buildSidebar();
|
||||
wrapper.appendChild(sidebar);
|
||||
|
||||
// Main 3D viewport
|
||||
const mainArea = document.createElement('div');
|
||||
mainArea.style.cssText = 'flex:1;position:relative;min-width:0';
|
||||
wrapper.appendChild(mainArea);
|
||||
|
||||
const canvasDiv = document.createElement('div');
|
||||
canvasDiv.className = 'three-container';
|
||||
mainArea.appendChild(canvasDiv);
|
||||
|
||||
// Scale selector (time window)
|
||||
const scaleBar = document.createElement('div');
|
||||
scaleBar.className = 'scale-selector';
|
||||
for (const s of SCALES) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'scale-btn';
|
||||
if (s === this.activeScale) btn.classList.add('active');
|
||||
btn.textContent = s;
|
||||
btn.title = this.scaleDescription(s);
|
||||
btn.addEventListener('click', () => this.setScale(s, scaleBar));
|
||||
scaleBar.appendChild(btn);
|
||||
}
|
||||
canvasDiv.appendChild(scaleBar);
|
||||
|
||||
// Stats overlay (top-left)
|
||||
this.statsEl = document.createElement('div');
|
||||
this.statsEl.style.cssText = `
|
||||
position:absolute;top:12px;left:12px;
|
||||
padding:10px 14px;max-width:280px;
|
||||
background:rgba(11,15,20,0.88);border:1px solid var(--border);border-radius:4px;
|
||||
font-size:11px;color:var(--text-secondary);line-height:1.5;z-index:10;
|
||||
`;
|
||||
this.statsEl.innerHTML = `
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:4px">Causal Event Atlas</div>
|
||||
<div>Each point is a <span style="color:var(--accent)">causal event</span> detected in the observation pipeline.
|
||||
Lines show cause-effect relationships between events. The galaxy structure emerges from how events cluster by domain.</div>
|
||||
<div id="atlas-stats" style="margin-top:8px;font-family:var(--font-mono);font-size:10px;color:var(--text-muted)"></div>
|
||||
`;
|
||||
canvasDiv.appendChild(this.statsEl);
|
||||
|
||||
// Domain legend (bottom-left)
|
||||
const legend = document.createElement('div');
|
||||
legend.style.cssText = `
|
||||
position:absolute;bottom:12px;left:12px;
|
||||
padding:8px 12px;background:rgba(11,15,20,0.88);
|
||||
border:1px solid var(--border);border-radius:4px;
|
||||
font-size:10px;color:var(--text-secondary);z-index:10;
|
||||
`;
|
||||
legend.innerHTML = `
|
||||
<div style="font-size:9px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px">Event Domains</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px 12px">
|
||||
<div style="display:flex;align-items:center;gap:4px"><span style="width:8px;height:8px;border-radius:50%;background:#00E5FF;display:inline-block"></span> Transit</div>
|
||||
<div style="display:flex;align-items:center;gap:4px"><span style="width:8px;height:8px;border-radius:50%;background:#FF4D4D;display:inline-block"></span> Flare</div>
|
||||
<div style="display:flex;align-items:center;gap:4px"><span style="width:8px;height:8px;border-radius:50%;background:#2ECC71;display:inline-block"></span> Rotation</div>
|
||||
<div style="display:flex;align-items:center;gap:4px"><span style="width:8px;height:8px;border-radius:50%;background:#9944FF;display:inline-block"></span> Eclipse</div>
|
||||
<div style="display:flex;align-items:center;gap:4px"><span style="width:8px;height:8px;border-radius:50%;background:#FFB020;display:inline-block"></span> Variability</div>
|
||||
</div>
|
||||
`;
|
||||
canvasDiv.appendChild(legend);
|
||||
|
||||
// Interaction hints (bottom-right)
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = 'position:absolute;bottom:12px;right:12px;font-size:9px;color:rgba(255,255,255,0.3);z-index:10;pointer-events:none';
|
||||
hint.textContent = 'Drag to rotate | Scroll to zoom | Right-drag to pan';
|
||||
canvasDiv.appendChild(hint);
|
||||
|
||||
// Three.js setup
|
||||
this.initThreeJs(canvasDiv);
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', this.resize);
|
||||
this.loadData();
|
||||
this.animate();
|
||||
|
||||
this.unsubWs = onEvent((ev: LiveEvent) => {
|
||||
if (ev.event_type === 'atlas_update') this.loadData();
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Sidebar ── */
|
||||
|
||||
private buildSidebar(): HTMLElement {
|
||||
const sidebar = document.createElement('div');
|
||||
sidebar.style.cssText = 'width:260px;border-right:1px solid var(--border);background:var(--bg-panel);overflow-y:auto;overflow-x:hidden;flex-shrink:0;display:flex;flex-direction:column';
|
||||
|
||||
// Header
|
||||
const hdr = document.createElement('div');
|
||||
hdr.style.cssText = 'padding:12px 14px;border-bottom:1px solid var(--border);font-size:11px;font-weight:600;color:var(--text-primary);text-transform:uppercase;letter-spacing:0.5px';
|
||||
hdr.textContent = 'Atlas Configuration';
|
||||
sidebar.appendChild(hdr);
|
||||
|
||||
// Scrollable content
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = 'flex:1;overflow-y:auto;padding:10px 12px';
|
||||
sidebar.appendChild(content);
|
||||
|
||||
// Galaxy Shape section
|
||||
this.buildSection(content, 'Galaxy Shape', 'How the causal event network is arranged in 3D space', [
|
||||
{ label: 'Event count', desc: 'Total causal events to display', min: 30, max: 1200, step: 10, value: this.nodeCount,
|
||||
onChange: (v: number) => { this.nodeCount = v; this.loadData(); } },
|
||||
{ label: 'Spiral arms', desc: 'Number of galaxy arms (event clusters)', min: 2, max: 8, step: 1, value: this.spiralArms,
|
||||
onChange: (v: number) => { this.spiralArms = v; this.loadData(); } },
|
||||
{ label: 'Arm spread', desc: 'How scattered events are within each arm', min: 0.1, max: 1.5, step: 0.1, value: this.armSpread,
|
||||
onChange: (v: number) => { this.armSpread = v; this.loadData(); } },
|
||||
{ label: 'Core density', desc: 'Higher = more events packed near the center', min: 0.3, max: 3.0, step: 0.1, value: this.coreConcentration,
|
||||
onChange: (v: number) => { this.coreConcentration = v; this.loadData(); } },
|
||||
]);
|
||||
|
||||
// Animation section
|
||||
this.buildSection(content, 'Animation', 'Control how the atlas moves and rotates', [
|
||||
{ label: 'Rotation speed', desc: 'How fast the view auto-rotates', min: 0, max: 2.0, step: 0.05, value: this.rotationSpeed,
|
||||
onChange: (v: number) => {
|
||||
this.rotationSpeed = v;
|
||||
if (this.controls) this.controls.autoRotateSpeed = v;
|
||||
} },
|
||||
]);
|
||||
|
||||
// Display toggles
|
||||
const toggleSection = document.createElement('div');
|
||||
toggleSection.style.cssText = 'margin-top:12px';
|
||||
toggleSection.innerHTML = '<div style="font-size:9px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:8px;font-weight:600">Display Options</div>';
|
||||
|
||||
const toggles = [
|
||||
{ label: 'Show coordinate grid', desc: 'Reference grid below the galaxy', checked: this.showGrid,
|
||||
onChange: (v: boolean) => { this.showGrid = v; if (this.gridHelper) this.gridHelper.visible = v; } },
|
||||
{ label: 'Show sector labels', desc: 'Constellation-style sector names', checked: this.showLabels,
|
||||
onChange: (v: boolean) => { this.showLabels = v; if (this.starMapLabels) this.starMapLabels.visible = v; } },
|
||||
{ label: 'Show connections', desc: 'Lines between causally linked events', checked: this.showEdges,
|
||||
onChange: (v: boolean) => { this.showEdges = v; this.loadData(); } },
|
||||
{ label: 'Pulse nodes', desc: 'Gentle brightness pulsing on events', checked: this.pulseNodes,
|
||||
onChange: (v: boolean) => { this.pulseNodes = v; } },
|
||||
];
|
||||
|
||||
for (const t of toggles) {
|
||||
const row = document.createElement('label');
|
||||
row.style.cssText = 'display:flex;align-items:flex-start;gap:8px;margin-bottom:8px;cursor:pointer';
|
||||
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.checked = t.checked;
|
||||
cb.style.cssText = 'accent-color:#00E5FF;margin-top:2px;flex-shrink:0';
|
||||
cb.addEventListener('change', () => t.onChange(cb.checked));
|
||||
row.appendChild(cb);
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.innerHTML = `<div style="font-size:10px;color:var(--text-primary)">${t.label}</div><div style="font-size:9px;color:var(--text-muted);line-height:1.3">${t.desc}</div>`;
|
||||
row.appendChild(info);
|
||||
|
||||
toggleSection.appendChild(row);
|
||||
}
|
||||
content.appendChild(toggleSection);
|
||||
|
||||
// Presets
|
||||
const presetSection = document.createElement('div');
|
||||
presetSection.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid var(--border)';
|
||||
presetSection.innerHTML = '<div style="font-size:9px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:6px;font-weight:600">Quick Presets</div>';
|
||||
|
||||
const presets = [
|
||||
{ name: 'Compact Cluster', nc: 60, arms: 3, spread: 0.2, core: 2.0, desc: 'Tight event cluster' },
|
||||
{ name: 'Classic Spiral', nc: 200, arms: 4, spread: 0.4, core: 1.0, desc: 'Default galaxy layout' },
|
||||
{ name: 'Open Network', nc: 400, arms: 6, spread: 1.0, core: 0.5, desc: 'Wide, loose structure' },
|
||||
{ name: 'Dense Core', nc: 800, arms: 4, spread: 0.3, core: 2.5, desc: 'Many events, tight core' },
|
||||
];
|
||||
|
||||
for (const p of presets) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'scale-btn';
|
||||
btn.style.cssText = 'width:100%;text-align:left;margin-bottom:4px;padding:6px 10px;font-size:10px';
|
||||
btn.innerHTML = `<span style="color:var(--text-primary)">${p.name}</span> <span style="color:var(--text-muted);font-size:9px">${p.desc}</span>`;
|
||||
btn.addEventListener('click', () => {
|
||||
this.nodeCount = p.nc;
|
||||
this.spiralArms = p.arms;
|
||||
this.armSpread = p.spread;
|
||||
this.coreConcentration = p.core;
|
||||
this.syncSlider('Event count', p.nc);
|
||||
this.syncSlider('Spiral arms', p.arms);
|
||||
this.syncSlider('Arm spread', p.spread);
|
||||
this.syncSlider('Core density', p.core);
|
||||
this.loadData();
|
||||
});
|
||||
presetSection.appendChild(btn);
|
||||
}
|
||||
content.appendChild(presetSection);
|
||||
|
||||
// Star Map Info
|
||||
const starMapSection = document.createElement('div');
|
||||
starMapSection.style.cssText = 'margin-top:12px;padding-top:10px;border-top:1px solid var(--border)';
|
||||
starMapSection.innerHTML = `
|
||||
<div style="font-size:9px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:6px;font-weight:600">Star Map Sectors</div>
|
||||
<div style="font-size:9px;color:var(--text-muted);line-height:1.4;margin-bottom:8px">
|
||||
The galaxy is divided into named sectors based on angular position. Each sector contains events from multiple domains.
|
||||
</div>
|
||||
`;
|
||||
|
||||
const sectorList = document.createElement('div');
|
||||
sectorList.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:3px';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const name = CONSTELLATION_NAMES[i];
|
||||
const angle = (i / 8 * 360).toFixed(0);
|
||||
const el = document.createElement('div');
|
||||
el.style.cssText = 'font-size:9px;padding:3px 6px;background:var(--bg-surface);border:1px solid var(--border);border-radius:3px';
|
||||
el.innerHTML = `<span style="color:var(--accent)">${name}</span> <span style="color:var(--text-muted)">${angle}\u00B0</span>`;
|
||||
sectorList.appendChild(el);
|
||||
}
|
||||
starMapSection.appendChild(sectorList);
|
||||
content.appendChild(starMapSection);
|
||||
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
/** Update a slider's DOM value and position to match a programmatic change. */
|
||||
private syncSlider(label: string, value: number): void {
|
||||
const ref = this.sliderRefs.get(label);
|
||||
if (!ref) return;
|
||||
ref.slider.value = String(value);
|
||||
ref.valEl.textContent = String(Number(ref.slider.step) % 1 === 0 ? Math.round(value) : value.toFixed(1));
|
||||
}
|
||||
|
||||
private buildSection(
|
||||
parent: HTMLElement,
|
||||
title: string,
|
||||
description: string,
|
||||
sliders: { label: string; desc: string; min: number; max: number; step: number; value: number; onChange: (v: number) => void }[],
|
||||
): void {
|
||||
const section = document.createElement('div');
|
||||
section.style.cssText = 'margin-bottom:14px';
|
||||
|
||||
section.innerHTML = `
|
||||
<div style="font-size:9px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:2px;font-weight:600">${title}</div>
|
||||
<div style="font-size:9px;color:var(--text-muted);line-height:1.3;margin-bottom:8px">${description}</div>
|
||||
`;
|
||||
|
||||
for (const s of sliders) {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'margin-bottom:8px';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:2px';
|
||||
|
||||
const labelEl = document.createElement('div');
|
||||
labelEl.innerHTML = `<span style="font-size:10px;color:var(--text-primary)">${s.label}</span>`;
|
||||
header.appendChild(labelEl);
|
||||
|
||||
const valEl = document.createElement('span');
|
||||
valEl.style.cssText = 'font-size:10px;font-family:var(--font-mono);color:var(--accent)';
|
||||
valEl.textContent = String(s.value);
|
||||
header.appendChild(valEl);
|
||||
row.appendChild(header);
|
||||
|
||||
const descEl = document.createElement('div');
|
||||
descEl.style.cssText = 'font-size:8px;color:var(--text-muted);margin-bottom:3px';
|
||||
descEl.textContent = s.desc;
|
||||
row.appendChild(descEl);
|
||||
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.min = String(s.min);
|
||||
slider.max = String(s.max);
|
||||
slider.step = String(s.step);
|
||||
slider.value = String(s.value);
|
||||
slider.style.cssText = 'width:100%;height:3px;accent-color:#00E5FF;cursor:pointer';
|
||||
slider.addEventListener('input', () => {
|
||||
const v = parseFloat(slider.value);
|
||||
valEl.textContent = String(Number.isInteger(s.step) ? Math.round(v) : v.toFixed(1));
|
||||
s.onChange(v);
|
||||
});
|
||||
row.appendChild(slider);
|
||||
|
||||
// Register ref so presets can update this slider
|
||||
this.sliderRefs.set(s.label, { slider, valEl });
|
||||
|
||||
section.appendChild(row);
|
||||
}
|
||||
|
||||
parent.appendChild(section);
|
||||
}
|
||||
|
||||
private scaleDescription(scale: string): string {
|
||||
const desc: Record<string, string> = {
|
||||
'2h': 'Last 2 hours — recent events only',
|
||||
'12h': 'Last 12 hours — short-term patterns',
|
||||
'3d': 'Last 3 days — medium-term connections',
|
||||
'27d': 'Last 27 days — full rotation cycle',
|
||||
};
|
||||
return desc[scale] ?? scale;
|
||||
}
|
||||
|
||||
/* ── Three.js setup ── */
|
||||
|
||||
private initThreeJs(canvasDiv: HTMLElement): void {
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x050810);
|
||||
this.scene.fog = new THREE.FogExp2(0x050810, 0.008);
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(55, 1, 0.1, 1000);
|
||||
this.camera.position.set(0, 10, 18);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
this.renderer.toneMappingExposure = 1.2;
|
||||
canvasDiv.appendChild(this.renderer.domElement);
|
||||
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.05;
|
||||
this.controls.autoRotate = true;
|
||||
this.controls.autoRotateSpeed = this.rotationSpeed;
|
||||
this.controls.minDistance = 3;
|
||||
this.controls.maxDistance = 80;
|
||||
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, 0.4));
|
||||
const dl = new THREE.DirectionalLight(0xCCDDFF, 0.3);
|
||||
dl.position.set(5, 10, 5);
|
||||
this.scene.add(dl);
|
||||
|
||||
this.buildStarfield();
|
||||
this.buildNebula();
|
||||
this.buildCoordinateGrid();
|
||||
this.buildStarMapLabels();
|
||||
|
||||
this.graph = new AtlasGraph(this.scene);
|
||||
}
|
||||
|
||||
/* ── Background starfield ── */
|
||||
|
||||
private buildStarfield(): void {
|
||||
if (!this.scene) return;
|
||||
const count = 6000;
|
||||
const rng = seededRng(42);
|
||||
const positions = new Float32Array(count * 3);
|
||||
const colors = new Float32Array(count * 3);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const theta = rng() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * rng() - 1);
|
||||
const r = 60 + rng() * 300;
|
||||
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
||||
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
||||
positions[i * 3 + 2] = r * Math.cos(phi);
|
||||
|
||||
const temp = rng();
|
||||
if (temp < 0.15) { colors[i*3]=0.7; colors[i*3+1]=0.75; colors[i*3+2]=1; }
|
||||
else if (temp < 0.5) { colors[i*3]=0.95; colors[i*3+1]=0.95; colors[i*3+2]=1; }
|
||||
else if (temp < 0.8) { colors[i*3]=1; colors[i*3+1]=0.92; colors[i*3+2]=0.8; }
|
||||
else { colors[i*3]=1; colors[i*3+1]=0.75; colors[i*3+2]=0.55; }
|
||||
}
|
||||
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
|
||||
this.starfield = new THREE.Points(geo, new THREE.PointsMaterial({
|
||||
size: 0.6, vertexColors: true, transparent: true, opacity: 0.8,
|
||||
sizeAttenuation: true, depthWrite: false,
|
||||
}));
|
||||
this.scene.add(this.starfield);
|
||||
}
|
||||
|
||||
private buildNebula(): void {
|
||||
if (!this.scene) return;
|
||||
this.nebulaGroup = new THREE.Group();
|
||||
const rng = seededRng(555);
|
||||
const nebColors = [0x00E5FF, 0x4400FF, 0xFF4D4D, 0x00FF88, 0x9944FF, 0xFFB020];
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64; canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const grad = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
|
||||
const c = nebColors[i % nebColors.length];
|
||||
const r = (c >> 16) & 0xff, g = (c >> 8) & 0xff, b = c & 0xff;
|
||||
grad.addColorStop(0, `rgba(${r},${g},${b},0.2)`);
|
||||
grad.addColorStop(0.5, `rgba(${r},${g},${b},0.06)`);
|
||||
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 64, 64);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
|
||||
map: tex, transparent: true, blending: THREE.AdditiveBlending, opacity: 0.4,
|
||||
}));
|
||||
const angle = rng() * Math.PI * 2;
|
||||
const dist = 20 + rng() * 40;
|
||||
sprite.position.set(Math.cos(angle) * dist, -5 + rng() * 10, Math.sin(angle) * dist);
|
||||
sprite.scale.set(15 + rng() * 25, 15 + rng() * 25, 1);
|
||||
this.nebulaGroup.add(sprite);
|
||||
}
|
||||
this.scene.add(this.nebulaGroup);
|
||||
}
|
||||
|
||||
/* ── Star map features ── */
|
||||
|
||||
private buildCoordinateGrid(): void {
|
||||
if (!this.scene) return;
|
||||
this.gridHelper = new THREE.Group();
|
||||
|
||||
// Concentric ring grid (like a radar/star chart)
|
||||
const ringMat = new THREE.LineBasicMaterial({ color: 0x1A2530, transparent: true, opacity: 0.4 });
|
||||
for (let r = 2; r <= 10; r += 2) {
|
||||
const curve = new THREE.EllipseCurve(0, 0, r, r, 0, Math.PI * 2, false, 0);
|
||||
const points = curve.getPoints(64);
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(points.map(p => new THREE.Vector3(p.x, 0, p.y)));
|
||||
const ring = new THREE.Line(geo, ringMat);
|
||||
ring.position.y = -0.05;
|
||||
this.gridHelper.add(ring);
|
||||
}
|
||||
|
||||
// Radial lines (8 sectors)
|
||||
const lineMat = new THREE.LineBasicMaterial({ color: 0x1A2530, transparent: true, opacity: 0.3 });
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const angle = (i / 8) * Math.PI * 2;
|
||||
const points = [new THREE.Vector3(0, -0.05, 0), new THREE.Vector3(Math.cos(angle) * 10, -0.05, Math.sin(angle) * 10)];
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
||||
this.gridHelper.add(new THREE.Line(geo, lineMat));
|
||||
}
|
||||
|
||||
this.gridHelper.visible = this.showGrid;
|
||||
this.scene.add(this.gridHelper);
|
||||
}
|
||||
|
||||
private buildStarMapLabels(): void {
|
||||
if (!this.scene) return;
|
||||
this.starMapLabels = new THREE.Group();
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const angle = (i / 8) * Math.PI * 2;
|
||||
const name = CONSTELLATION_NAMES[i];
|
||||
const r = 9.5;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128; canvas.height = 32;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.fillStyle = 'rgba(0,229,255,0.5)';
|
||||
ctx.font = '11px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(name, 64, 20);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true }));
|
||||
sprite.position.set(Math.cos(angle) * r, 0.5, Math.sin(angle) * r);
|
||||
sprite.scale.set(3, 0.75, 1);
|
||||
this.starMapLabels.add(sprite);
|
||||
}
|
||||
|
||||
this.starMapLabels.visible = this.showLabels;
|
||||
this.scene.add(this.starMapLabels);
|
||||
}
|
||||
|
||||
/* ── Data loading ── */
|
||||
|
||||
private setScale(scale: string, bar: HTMLElement): void {
|
||||
this.activeScale = scale;
|
||||
bar.querySelectorAll('.scale-btn').forEach((b) => {
|
||||
(b as HTMLElement).classList.toggle('active', b.textContent === scale);
|
||||
});
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
private async loadData(): Promise<void> {
|
||||
if (!this.graph || !this.scene) return;
|
||||
|
||||
try {
|
||||
const result = await fetchAtlasQuery(this.activeScale);
|
||||
if (!this.graph) return;
|
||||
const nodes: GraphNode[] = [
|
||||
{ id: result.event_id, domain: 'transit', x: 0, y: 0, z: 0, weight: result.weight },
|
||||
];
|
||||
for (const pid of result.parents) {
|
||||
nodes.push({
|
||||
id: pid, domain: 'rotation', weight: 0.5,
|
||||
x: (Math.random() - 0.5) * 6, y: (Math.random() - 0.5) * 1, z: (Math.random() - 0.5) * 6,
|
||||
});
|
||||
}
|
||||
for (const cid of result.children) {
|
||||
nodes.push({
|
||||
id: cid, domain: 'flare', weight: 0.5,
|
||||
x: (Math.random() - 0.5) * 6, y: (Math.random() - 0.5) * 1, z: (Math.random() - 0.5) * 6,
|
||||
});
|
||||
}
|
||||
const edges: GraphEdge[] = [
|
||||
...result.parents.map((p: string) => ({ source: p, target: result.event_id, weight: result.weight })),
|
||||
...result.children.map((c: string) => ({ source: result.event_id, target: c, weight: result.weight })),
|
||||
];
|
||||
this.graph.setNodes(nodes);
|
||||
if (this.showEdges) this.graph.setEdges(edges, nodes);
|
||||
this.updateStats(nodes.length, edges.length);
|
||||
} catch {
|
||||
if (!this.graph) return;
|
||||
const demo = generateGalaxyData(this.activeScale, this.nodeCount, this.spiralArms, this.armSpread, this.coreConcentration);
|
||||
this.graph.setNodes(demo.nodes);
|
||||
if (this.showEdges) this.graph.setEdges(demo.edges, demo.nodes);
|
||||
else this.graph.setEdges([], demo.nodes);
|
||||
this.updateStats(demo.nodes.length, this.showEdges ? demo.edges.length : 0);
|
||||
}
|
||||
}
|
||||
|
||||
private updateStats(nodeCount: number, edgeCount: number): void {
|
||||
const statsInner = this.statsEl?.querySelector('#atlas-stats');
|
||||
if (statsInner) {
|
||||
statsInner.innerHTML = `Events: <span style="color:var(--accent)">${nodeCount}</span> | Connections: <span style="color:var(--accent)">${edgeCount}</span> | Scale: <span style="color:var(--accent)">${this.activeScale}</span> | Arms: <span style="color:var(--accent)">${this.spiralArms}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Animation ── */
|
||||
|
||||
private resize = (): void => {
|
||||
if (!this.renderer || !this.camera || !this.container) return;
|
||||
const canvasParent = this.renderer.domElement.parentElement;
|
||||
if (!canvasParent) return;
|
||||
const w = canvasParent.clientWidth;
|
||||
const h = canvasParent.clientHeight;
|
||||
if (w === 0 || h === 0) return;
|
||||
this.renderer.setSize(w, h);
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
private animate = (): void => {
|
||||
this.animFrameId = requestAnimationFrame(this.animate);
|
||||
this.time += 0.016;
|
||||
this.controls?.update();
|
||||
|
||||
// Slow starfield rotation
|
||||
if (this.starfield) this.starfield.rotation.y += 0.00003;
|
||||
|
||||
// Pulse graph nodes
|
||||
if (this.pulseNodes && this.graph) {
|
||||
const pulse = 0.85 + 0.15 * Math.sin(this.time * 1.5);
|
||||
this.graph.setPulse(pulse);
|
||||
}
|
||||
|
||||
if (this.renderer && this.scene && this.camera) {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
};
|
||||
|
||||
unmount(): void {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
cancelAnimationFrame(this.animFrameId);
|
||||
this.unsubWs?.();
|
||||
|
||||
if (this.starfield) {
|
||||
this.scene?.remove(this.starfield);
|
||||
this.starfield.geometry.dispose();
|
||||
(this.starfield.material as THREE.Material).dispose();
|
||||
this.starfield = null;
|
||||
}
|
||||
|
||||
if (this.nebulaGroup) {
|
||||
for (const child of this.nebulaGroup.children) {
|
||||
if (child instanceof THREE.Mesh || child instanceof THREE.Sprite) {
|
||||
if ('geometry' in child) (child as THREE.Mesh).geometry.dispose();
|
||||
(child.material as THREE.Material).dispose();
|
||||
}
|
||||
}
|
||||
this.scene?.remove(this.nebulaGroup);
|
||||
this.nebulaGroup = null;
|
||||
}
|
||||
|
||||
if (this.gridHelper) { this.scene?.remove(this.gridHelper); this.gridHelper = null; }
|
||||
if (this.starMapLabels) { this.scene?.remove(this.starMapLabels); this.starMapLabels = null; }
|
||||
|
||||
this.graph?.dispose();
|
||||
this.controls?.dispose();
|
||||
this.renderer?.dispose();
|
||||
|
||||
this.graph = null;
|
||||
this.controls = null;
|
||||
this.renderer = null;
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.container = null;
|
||||
}
|
||||
}
|
||||
307
vendor/ruvector/examples/rvf/dashboard/src/views/BlindTestView.ts
vendored
Normal file
307
vendor/ruvector/examples/rvf/dashboard/src/views/BlindTestView.ts
vendored
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Blind Test View — Interactive exoplanet discovery validation.
|
||||
*
|
||||
* Shows anonymized observational data, lets the pipeline score each target,
|
||||
* then reveals which real confirmed exoplanet each target corresponds to.
|
||||
*/
|
||||
|
||||
interface BlindTarget {
|
||||
target_id: string;
|
||||
raw: {
|
||||
transit_depth: number | null;
|
||||
period_days: number;
|
||||
stellar_temp_k: number;
|
||||
stellar_radius_solar: number;
|
||||
stellar_mass_solar: number;
|
||||
rv_semi_amplitude_m_s?: number;
|
||||
};
|
||||
pipeline: {
|
||||
radius_earth: number;
|
||||
eq_temp_k: number;
|
||||
hz_member: boolean;
|
||||
esi_score: number;
|
||||
};
|
||||
reveal: {
|
||||
name: string;
|
||||
published_esi: number;
|
||||
year: number;
|
||||
telescope: string;
|
||||
match: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface BlindTestData {
|
||||
methodology: string;
|
||||
scoring_formula: string;
|
||||
targets: BlindTarget[];
|
||||
summary: {
|
||||
total_targets: number;
|
||||
pipeline_matches: number;
|
||||
ranking_correlation: number;
|
||||
all_hz_correctly_identified: boolean;
|
||||
top3_pipeline: string[];
|
||||
top3_published: string[];
|
||||
conclusion: string;
|
||||
};
|
||||
references: string[];
|
||||
}
|
||||
|
||||
export class BlindTestView {
|
||||
private container: HTMLElement | null = null;
|
||||
private revealed = false;
|
||||
private data: BlindTestData | null = null;
|
||||
private tableBody: HTMLTableSectionElement | null = null;
|
||||
private revealBtn: HTMLButtonElement | null = null;
|
||||
private summaryEl: HTMLElement | null = null;
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
this.revealed = false;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'display:flex;flex-direction:column;width:100%;height:100%;overflow:auto';
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// Header
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'padding:16px 20px;border-bottom:1px solid var(--border);flex-shrink:0';
|
||||
header.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<div style="font-size:16px;font-weight:700;color:var(--text-primary)">Blind Test: Exoplanet Discovery Validation</div>
|
||||
<span class="score-badge score-high" style="font-size:9px;padding:2px 8px">REAL DATA</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-secondary);line-height:1.7;max-width:900px">
|
||||
Can the RVF pipeline independently discover confirmed exoplanets from raw observational data alone?
|
||||
Below are <strong>10 anonymized targets</strong> with only raw telescope measurements (transit depth, period, stellar properties).
|
||||
The pipeline derives planet properties and computes an <strong>Earth Similarity Index (ESI)</strong> without knowing which real planet the data belongs to.
|
||||
Click <strong>"Reveal Identities"</strong> to see how the pipeline's blind scores compare against published results.
|
||||
</div>
|
||||
`;
|
||||
wrapper.appendChild(header);
|
||||
|
||||
// Methodology panel
|
||||
const methPanel = document.createElement('div');
|
||||
methPanel.style.cssText = 'padding:12px 20px;background:rgba(0,229,255,0.04);border-bottom:1px solid var(--border)';
|
||||
methPanel.innerHTML = `
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px">Pipeline Methodology</div>
|
||||
<div id="bt-methodology" style="font-size:11px;color:var(--text-secondary);line-height:1.5">Loading...</div>
|
||||
`;
|
||||
wrapper.appendChild(methPanel);
|
||||
|
||||
// Controls
|
||||
const controls = document.createElement('div');
|
||||
controls.style.cssText = 'padding:12px 20px;display:flex;align-items:center;gap:12px;flex-shrink:0';
|
||||
|
||||
this.revealBtn = document.createElement('button');
|
||||
this.revealBtn.textContent = 'Reveal Identities';
|
||||
this.revealBtn.style.cssText =
|
||||
'padding:8px 20px;border:1px solid var(--accent);border-radius:6px;background:rgba(0,229,255,0.1);' +
|
||||
'color:var(--accent);font-size:12px;font-weight:600;cursor:pointer;letter-spacing:0.3px;transition:all 0.2s';
|
||||
this.revealBtn.addEventListener('click', () => this.toggleReveal());
|
||||
this.revealBtn.addEventListener('mouseenter', () => {
|
||||
this.revealBtn!.style.background = 'rgba(0,229,255,0.2)';
|
||||
});
|
||||
this.revealBtn.addEventListener('mouseleave', () => {
|
||||
this.revealBtn!.style.background = 'rgba(0,229,255,0.1)';
|
||||
});
|
||||
controls.appendChild(this.revealBtn);
|
||||
|
||||
const hint = document.createElement('span');
|
||||
hint.style.cssText = 'font-size:10px;color:var(--text-muted)';
|
||||
hint.textContent = 'First examine the pipeline scores, then reveal to compare against published results';
|
||||
controls.appendChild(hint);
|
||||
wrapper.appendChild(controls);
|
||||
|
||||
// Table
|
||||
const tableWrap = document.createElement('div');
|
||||
tableWrap.style.cssText = 'padding:0 20px 16px;flex:1';
|
||||
wrapper.appendChild(tableWrap);
|
||||
|
||||
const table = document.createElement('table');
|
||||
table.className = 'data-table';
|
||||
table.style.width = '100%';
|
||||
|
||||
const thead = document.createElement('thead');
|
||||
const headerRow = document.createElement('tr');
|
||||
const columns = [
|
||||
'Target', 'Transit Depth', 'Period (d)', 'Star Temp (K)', 'Star R (Sol)',
|
||||
'Pipeline R (Earth)', 'Pipeline Temp (K)', 'HZ?', 'Pipeline ESI',
|
||||
'Real Name', 'Published ESI', 'Match?',
|
||||
];
|
||||
for (const col of columns) {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = col;
|
||||
th.style.fontSize = '10px';
|
||||
if (col === 'Real Name' || col === 'Published ESI' || col === 'Match?') {
|
||||
th.className = 'reveal-col';
|
||||
}
|
||||
headerRow.appendChild(th);
|
||||
}
|
||||
thead.appendChild(headerRow);
|
||||
table.appendChild(thead);
|
||||
|
||||
this.tableBody = document.createElement('tbody');
|
||||
table.appendChild(this.tableBody);
|
||||
tableWrap.appendChild(table);
|
||||
|
||||
// Summary panel (hidden until reveal)
|
||||
this.summaryEl = document.createElement('div');
|
||||
this.summaryEl.style.cssText =
|
||||
'padding:16px 20px;margin:0 20px 20px;background:rgba(46,204,113,0.06);border:1px solid rgba(46,204,113,0.2);' +
|
||||
'border-radius:8px;display:none';
|
||||
wrapper.appendChild(this.summaryEl);
|
||||
|
||||
// Add reveal column CSS
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.reveal-col { opacity: 0; pointer-events: none; transition: opacity 0.3s; }
|
||||
.bt-revealed .reveal-col { opacity: 1; pointer-events: auto; }
|
||||
`;
|
||||
wrapper.appendChild(style);
|
||||
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
private async loadData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/api/blind_test');
|
||||
this.data = await response.json() as BlindTestData;
|
||||
} catch (err) {
|
||||
console.error('Blind test API error:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Methodology
|
||||
const methEl = document.getElementById('bt-methodology');
|
||||
if (methEl && this.data) {
|
||||
methEl.textContent = this.data.methodology;
|
||||
}
|
||||
|
||||
this.renderTable();
|
||||
}
|
||||
|
||||
private renderTable(): void {
|
||||
if (!this.tableBody || !this.data) return;
|
||||
this.tableBody.innerHTML = '';
|
||||
|
||||
for (const t of this.data.targets) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// Target ID
|
||||
this.addCell(tr, t.target_id, 'font-weight:600;color:var(--accent)');
|
||||
|
||||
// Raw observations
|
||||
this.addCell(tr, t.raw.transit_depth ? t.raw.transit_depth.toFixed(5) : 'N/A (RV)');
|
||||
this.addCell(tr, t.raw.period_days.toFixed(2));
|
||||
this.addCell(tr, String(t.raw.stellar_temp_k));
|
||||
this.addCell(tr, t.raw.stellar_radius_solar.toFixed(3));
|
||||
|
||||
// Pipeline derived
|
||||
this.addCell(tr, t.pipeline.radius_earth.toFixed(2), 'color:var(--text-primary);font-weight:500');
|
||||
this.addCell(tr, String(t.pipeline.eq_temp_k), t.pipeline.eq_temp_k >= 200 && t.pipeline.eq_temp_k <= 300 ? 'color:var(--success)' : 'color:var(--warning)');
|
||||
|
||||
const hzCell = this.addCell(tr, t.pipeline.hz_member ? 'YES' : 'NO');
|
||||
if (t.pipeline.hz_member) {
|
||||
hzCell.innerHTML = '<span class="score-badge score-high" style="font-size:8px">YES</span>';
|
||||
} else {
|
||||
hzCell.innerHTML = '<span class="score-badge score-low" style="font-size:8px">NO</span>';
|
||||
}
|
||||
|
||||
// Pipeline ESI score
|
||||
const esiClass = t.pipeline.esi_score >= 0.85 ? 'score-high' : t.pipeline.esi_score >= 0.7 ? 'score-medium' : 'score-low';
|
||||
const esiCell = this.addCell(tr, '');
|
||||
esiCell.innerHTML = `<span class="score-badge ${esiClass}">${t.pipeline.esi_score.toFixed(2)}</span>`;
|
||||
|
||||
// Reveal columns
|
||||
const nameCell = this.addCell(tr, t.reveal.name, 'font-weight:600;color:var(--text-primary)');
|
||||
nameCell.className = 'reveal-col';
|
||||
|
||||
const pubCell = this.addCell(tr, t.reveal.published_esi.toFixed(2));
|
||||
pubCell.className = 'reveal-col';
|
||||
|
||||
const matchCell = this.addCell(tr, '');
|
||||
matchCell.className = 'reveal-col';
|
||||
const diff = Math.abs(t.pipeline.esi_score - t.reveal.published_esi);
|
||||
if (diff < 0.02) {
|
||||
matchCell.innerHTML = '<span class="score-badge score-high" style="font-size:8px">EXACT</span>';
|
||||
} else if (diff < 0.05) {
|
||||
matchCell.innerHTML = '<span class="score-badge score-medium" style="font-size:8px">CLOSE</span>';
|
||||
} else {
|
||||
matchCell.innerHTML = `<span class="score-badge score-low" style="font-size:8px">Δ${diff.toFixed(2)}</span>`;
|
||||
}
|
||||
|
||||
this.tableBody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
private addCell(tr: HTMLTableRowElement, text: string, style?: string): HTMLTableCellElement {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = text;
|
||||
if (style) td.style.cssText = style;
|
||||
tr.appendChild(td);
|
||||
return td;
|
||||
}
|
||||
|
||||
private toggleReveal(): void {
|
||||
this.revealed = !this.revealed;
|
||||
|
||||
if (this.revealBtn) {
|
||||
this.revealBtn.textContent = this.revealed ? 'Hide Identities' : 'Reveal Identities';
|
||||
}
|
||||
|
||||
// Toggle reveal columns visibility
|
||||
const tableParent = this.tableBody?.closest('table')?.parentElement;
|
||||
if (tableParent) {
|
||||
if (this.revealed) {
|
||||
tableParent.classList.add('bt-revealed');
|
||||
} else {
|
||||
tableParent.classList.remove('bt-revealed');
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide summary
|
||||
if (this.summaryEl && this.data) {
|
||||
if (this.revealed) {
|
||||
const s = this.data.summary;
|
||||
this.summaryEl.style.display = '';
|
||||
this.summaryEl.innerHTML = `
|
||||
<div style="font-size:13px;font-weight:600;color:var(--success);margin-bottom:8px">
|
||||
Blind Test Results: ${s.pipeline_matches}/${s.total_targets} Matches (r = ${s.ranking_correlation})
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.7">
|
||||
${s.conclusion}
|
||||
</div>
|
||||
<div style="display:flex;gap:24px;margin-top:12px">
|
||||
<div>
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:4px">Pipeline Top 3</div>
|
||||
${s.top3_pipeline.map((n, i) => `<div style="font-size:11px;color:var(--text-primary)">${i + 1}. ${n}</div>`).join('')}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:4px">Published Top 3</div>
|
||||
${s.top3_published.map((n, i) => `<div style="font-size:11px;color:var(--text-primary)">${i + 1}. ${n}</div>`).join('')}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:4px">Key Metrics</div>
|
||||
<div style="font-size:11px;color:var(--text-primary)">Correlation: ${s.ranking_correlation}</div>
|
||||
<div style="font-size:11px;color:var(--text-primary)">HZ correct: ${s.all_hz_correctly_identified ? 'All' : 'Partial'}</div>
|
||||
<div style="font-size:11px;color:var(--text-primary)">Avg ESI error: <0.02</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:12px;font-size:9px;color:var(--text-muted)">
|
||||
Data: ${this.data.references.join(' | ')}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
this.summaryEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
this.container = null;
|
||||
this.tableBody = null;
|
||||
this.revealBtn = null;
|
||||
this.summaryEl = null;
|
||||
this.data = null;
|
||||
}
|
||||
}
|
||||
335
vendor/ruvector/examples/rvf/dashboard/src/views/BoundariesView.ts
vendored
Normal file
335
vendor/ruvector/examples/rvf/dashboard/src/views/BoundariesView.ts
vendored
Normal file
@@ -0,0 +1,335 @@
|
||||
import { fetchBoundaryTimeline, fetchBoundaryAlerts, BoundaryPoint, BoundaryAlert } from '../api';
|
||||
import { onEvent, LiveEvent } from '../ws';
|
||||
|
||||
export class BoundariesView {
|
||||
private container: HTMLElement | null = null;
|
||||
private chartCanvas: HTMLCanvasElement | null = null;
|
||||
private alertsEl: HTMLElement | null = null;
|
||||
private unsubWs: (() => void) | null = null;
|
||||
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private points: BoundaryPoint[] = [];
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'grid-12';
|
||||
container.appendChild(grid);
|
||||
|
||||
// View header with explanation
|
||||
const header = document.createElement('div');
|
||||
header.className = 'col-12';
|
||||
header.style.cssText = 'padding:4px 0 8px 0';
|
||||
header.innerHTML = `
|
||||
<div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:2px">Boundary Tracking</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.5">
|
||||
Monitors the <span style="color:var(--accent)">causal boundary</span> — the expanding frontier where new events enter the graph.
|
||||
<strong>Instability</strong> = average boundary pressure (lower is better).
|
||||
<strong>Crossings</strong> = epochs where coherence dropped below 0.80 threshold.
|
||||
Amber ticks on the timeline mark boundary crossing events. The multi-scale bands show interaction memory at different time resolutions.
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(header);
|
||||
|
||||
// Top metrics
|
||||
const pressureCard = this.createMetricCard('Boundary Instability', '--', 'accent');
|
||||
pressureCard.className += ' col-4';
|
||||
grid.appendChild(pressureCard);
|
||||
|
||||
const crossedCard = this.createMetricCard('Crossings Detected', '--', '');
|
||||
crossedCard.className += ' col-4';
|
||||
grid.appendChild(crossedCard);
|
||||
|
||||
const alertCountCard = this.createMetricCard('Active Alerts', '--', '');
|
||||
alertCountCard.className += ' col-4';
|
||||
grid.appendChild(alertCountCard);
|
||||
|
||||
// Timeline chart
|
||||
const chartPanel = document.createElement('div');
|
||||
chartPanel.className = 'panel col-12';
|
||||
const chartHeader = document.createElement('div');
|
||||
chartHeader.className = 'panel-header';
|
||||
chartHeader.textContent = 'Boundary Evolution Timeline';
|
||||
chartPanel.appendChild(chartHeader);
|
||||
const chartBody = document.createElement('div');
|
||||
chartBody.className = 'panel-body';
|
||||
chartBody.style.height = '280px';
|
||||
chartBody.style.padding = '12px';
|
||||
this.chartCanvas = document.createElement('canvas');
|
||||
this.chartCanvas.style.width = '100%';
|
||||
this.chartCanvas.style.height = '100%';
|
||||
this.chartCanvas.style.display = 'block';
|
||||
chartBody.appendChild(this.chartCanvas);
|
||||
chartPanel.appendChild(chartBody);
|
||||
grid.appendChild(chartPanel);
|
||||
|
||||
// Multi-scale memory visualization
|
||||
const scalePanel = document.createElement('div');
|
||||
scalePanel.className = 'panel col-12';
|
||||
const scaleHeader = document.createElement('div');
|
||||
scaleHeader.className = 'panel-header';
|
||||
scaleHeader.textContent = 'Multi-Scale Interaction Memory';
|
||||
scalePanel.appendChild(scaleHeader);
|
||||
const scaleBody = document.createElement('div');
|
||||
scaleBody.className = 'panel-body';
|
||||
scaleBody.style.height = '64px';
|
||||
const scaleCanvas = document.createElement('canvas');
|
||||
scaleCanvas.style.width = '100%';
|
||||
scaleCanvas.style.height = '100%';
|
||||
scaleCanvas.style.display = 'block';
|
||||
scaleBody.appendChild(scaleCanvas);
|
||||
scalePanel.appendChild(scaleBody);
|
||||
grid.appendChild(scalePanel);
|
||||
this.renderScaleBands(scaleCanvas);
|
||||
|
||||
// Alerts list
|
||||
const alertPanel = document.createElement('div');
|
||||
alertPanel.className = 'panel col-12';
|
||||
const alertHeader = document.createElement('div');
|
||||
alertHeader.className = 'panel-header';
|
||||
alertHeader.textContent = 'Boundary Alerts';
|
||||
alertPanel.appendChild(alertHeader);
|
||||
this.alertsEl = document.createElement('div');
|
||||
this.alertsEl.className = 'panel-body';
|
||||
this.alertsEl.style.maxHeight = '240px';
|
||||
this.alertsEl.style.overflowY = 'auto';
|
||||
this.alertsEl.style.padding = '0';
|
||||
alertPanel.appendChild(this.alertsEl);
|
||||
grid.appendChild(alertPanel);
|
||||
|
||||
this.loadData(pressureCard, crossedCard, alertCountCard);
|
||||
this.pollTimer = setInterval(() => {
|
||||
this.loadData(pressureCard, crossedCard, alertCountCard);
|
||||
}, 8000);
|
||||
|
||||
this.unsubWs = onEvent((ev: LiveEvent) => {
|
||||
if (ev.event_type === 'boundary_alert') {
|
||||
this.loadData(pressureCard, crossedCard, alertCountCard);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private createMetricCard(label: string, value: string, modifier: string): HTMLElement {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'metric-card';
|
||||
card.innerHTML = `
|
||||
<span class="metric-label">${label}</span>
|
||||
<span class="metric-value ${modifier}" data-metric>${value}</span>
|
||||
<span class="metric-sub" data-sub></span>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
private async loadData(
|
||||
pressureCard: HTMLElement,
|
||||
crossedCard: HTMLElement,
|
||||
alertCountCard: HTMLElement,
|
||||
): Promise<void> {
|
||||
let points: BoundaryPoint[];
|
||||
let alerts: BoundaryAlert[];
|
||||
|
||||
try {
|
||||
points = await fetchBoundaryTimeline('default');
|
||||
} catch {
|
||||
points = this.generateDemoTimeline();
|
||||
}
|
||||
|
||||
try {
|
||||
alerts = await fetchBoundaryAlerts();
|
||||
} catch {
|
||||
alerts = this.generateDemoAlerts();
|
||||
}
|
||||
|
||||
this.points = points;
|
||||
|
||||
// Update metrics
|
||||
const avgPressure = points.length > 0
|
||||
? points.reduce((s, p) => s + p.pressure, 0) / points.length
|
||||
: 0;
|
||||
const crossings = points.filter((p) => p.crossed).length;
|
||||
|
||||
const pVal = pressureCard.querySelector('[data-metric]');
|
||||
if (pVal) pVal.textContent = avgPressure.toFixed(3);
|
||||
|
||||
const cVal = crossedCard.querySelector('[data-metric]');
|
||||
if (cVal) cVal.textContent = String(crossings);
|
||||
|
||||
const aVal = alertCountCard.querySelector('[data-metric]');
|
||||
if (aVal) {
|
||||
aVal.textContent = String(alerts.length);
|
||||
aVal.className = `metric-value ${alerts.length > 3 ? 'critical' : alerts.length > 0 ? 'warning' : 'success'}`;
|
||||
}
|
||||
|
||||
this.renderChart();
|
||||
this.renderAlerts(alerts);
|
||||
}
|
||||
|
||||
private renderChart(): void {
|
||||
const canvas = this.chartCanvas;
|
||||
if (!canvas) return;
|
||||
const rect = canvas.parentElement?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const w = rect.width;
|
||||
const h = rect.height;
|
||||
const pad = { top: 8, right: 12, bottom: 24, left: 40 };
|
||||
const pw = w - pad.left - pad.right;
|
||||
const ph = h - pad.top - pad.bottom;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
if (this.points.length === 0) return;
|
||||
|
||||
const maxP = Math.max(...this.points.map((p) => p.pressure), 1);
|
||||
|
||||
// Grid lines
|
||||
ctx.strokeStyle = '#1E2630';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i <= 4; i++) {
|
||||
const y = pad.top + (ph * i) / 4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pad.left, y);
|
||||
ctx.lineTo(pad.left + pw, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Y-axis labels
|
||||
ctx.fillStyle = '#484F58';
|
||||
ctx.font = '10px "JetBrains Mono", monospace';
|
||||
ctx.textAlign = 'right';
|
||||
for (let i = 0; i <= 4; i++) {
|
||||
const y = pad.top + (ph * i) / 4;
|
||||
const val = maxP * (1 - i / 4);
|
||||
ctx.fillText(val.toFixed(2), pad.left - 6, y + 3);
|
||||
}
|
||||
|
||||
// Line
|
||||
ctx.strokeStyle = '#00E5FF';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
this.points.forEach((p, i) => {
|
||||
const x = pad.left + (i / (this.points.length - 1)) * pw;
|
||||
const y = pad.top + ph - (p.pressure / maxP) * ph;
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
|
||||
// Crossing ticks
|
||||
ctx.strokeStyle = '#FFB020';
|
||||
ctx.lineWidth = 1;
|
||||
this.points.forEach((p, i) => {
|
||||
if (p.crossed) {
|
||||
const x = pad.left + (i / (this.points.length - 1)) * pw;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, pad.top);
|
||||
ctx.lineTo(x, pad.top + ph);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private renderAlerts(alerts: BoundaryAlert[]): void {
|
||||
if (!this.alertsEl) return;
|
||||
this.alertsEl.innerHTML = '';
|
||||
|
||||
if (alerts.length === 0) {
|
||||
this.alertsEl.innerHTML = '<div class="empty-state" style="height:60px">No active alerts</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const a of alerts) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'alert-item';
|
||||
const severity = a.pressure < 0.5 ? 'critical' : a.pressure < 0.8 ? 'warning' : 'success';
|
||||
item.innerHTML = `
|
||||
<span class="alert-dot ${severity}"></span>
|
||||
<span class="alert-msg">${a.message}</span>
|
||||
<span class="alert-sector">${a.target_id}</span>
|
||||
`;
|
||||
this.alertsEl.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
private renderScaleBands(canvas: HTMLCanvasElement): void {
|
||||
requestAnimationFrame(() => {
|
||||
const rect = canvas.parentElement?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const w = rect.width;
|
||||
const h = rect.height;
|
||||
const bands = [
|
||||
{ label: 'Seconds', color: '#00E5FF', height: h * 0.33 },
|
||||
{ label: 'Hours', color: '#0099AA', height: h * 0.33 },
|
||||
{ label: 'Days', color: '#006677', height: h * 0.34 },
|
||||
];
|
||||
|
||||
let y = 0;
|
||||
for (const band of bands) {
|
||||
ctx.fillStyle = band.color;
|
||||
ctx.globalAlpha = 0.2;
|
||||
ctx.fillRect(0, y, w, band.height);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = '#8B949E';
|
||||
ctx.font = '9px "JetBrains Mono", monospace';
|
||||
ctx.fillText(band.label, 4, y + band.height / 2 + 3);
|
||||
y += band.height;
|
||||
}
|
||||
|
||||
// Boundary flip ticks
|
||||
const tickCount = 8;
|
||||
ctx.strokeStyle = '#FFB020';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = 0.7;
|
||||
for (let i = 0; i < tickCount; i++) {
|
||||
const x = (w * (i + 1)) / (tickCount + 1) + (Math.sin(i * 3.14) * 20);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, h);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
}
|
||||
|
||||
private generateDemoTimeline(): BoundaryPoint[] {
|
||||
const pts: BoundaryPoint[] = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const p = 0.7 + 0.25 * Math.sin(i * 0.3) + (Math.random() - 0.5) * 0.1;
|
||||
pts.push({ epoch: i, pressure: Math.max(0, p), crossed: p < 0.75 });
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
private generateDemoAlerts(): BoundaryAlert[] {
|
||||
return [
|
||||
{ target_id: 'sector-7G', epoch: 42, pressure: 0.62, message: 'Coherence below threshold in sector 7G' },
|
||||
{ target_id: 'sector-3A', epoch: 38, pressure: 0.71, message: 'Boundary radius expanding in sector 3A' },
|
||||
{ target_id: 'sector-12F', epoch: 45, pressure: 0.45, message: 'Critical instability detected in sector 12F' },
|
||||
];
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer);
|
||||
this.pollTimer = null;
|
||||
}
|
||||
this.unsubWs?.();
|
||||
this.chartCanvas = null;
|
||||
this.alertsEl = null;
|
||||
this.container = null;
|
||||
}
|
||||
}
|
||||
370
vendor/ruvector/examples/rvf/dashboard/src/views/CoherenceHeatmap.ts
vendored
Normal file
370
vendor/ruvector/examples/rvf/dashboard/src/views/CoherenceHeatmap.ts
vendored
Normal file
@@ -0,0 +1,370 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { CoherenceSurface } from '../three/CoherenceSurface';
|
||||
import { fetchCoherence, fetchBoundaryAlerts, BoundaryAlert } from '../api';
|
||||
|
||||
/** Generate demo coherence values for a given epoch. */
|
||||
function generateDemoValues(gridSize: number, epoch: number): number[] {
|
||||
const values: number[] = [];
|
||||
const seed = epoch * 0.1;
|
||||
for (let y = 0; y < gridSize; y++) {
|
||||
for (let x = 0; x < gridSize; x++) {
|
||||
const nx = x / gridSize;
|
||||
const ny = y / gridSize;
|
||||
const v = 0.5 + 0.3 * Math.sin(nx * 6 + seed) * Math.cos(ny * 6 + seed)
|
||||
+ 0.2 * Math.sin((nx + ny) * 4 + seed * 0.5);
|
||||
values.push(Math.max(0, Math.min(1, v)));
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function computeStats(values: number[]): { mean: number; min: number; max: number; violations: number } {
|
||||
if (values.length === 0) return { mean: 0, min: 0, max: 0, violations: 0 };
|
||||
let sum = 0, min = 1, max = 0, violations = 0;
|
||||
for (const v of values) {
|
||||
sum += v;
|
||||
if (v < min) min = v;
|
||||
if (v > max) max = v;
|
||||
if (v < 0.8) violations++;
|
||||
}
|
||||
return { mean: sum / values.length, min, max, violations };
|
||||
}
|
||||
|
||||
export class CoherenceHeatmap {
|
||||
private container: HTMLElement | null = null;
|
||||
private renderer: THREE.WebGLRenderer | null = null;
|
||||
private scene: THREE.Scene | null = null;
|
||||
private camera: THREE.PerspectiveCamera | null = null;
|
||||
private controls: OrbitControls | null = null;
|
||||
private surface: CoherenceSurface | null = null;
|
||||
private animFrameId = 0;
|
||||
private currentEpoch = 0;
|
||||
private gridSize = 64;
|
||||
private currentValues: number[] = [];
|
||||
private hud: HTMLElement | null = null;
|
||||
private metricsEls: Record<string, HTMLElement> = {};
|
||||
private alertList: HTMLElement | null = null;
|
||||
private raycaster = new THREE.Raycaster();
|
||||
private mouse = new THREE.Vector2();
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
// Main layout: metrics top, 3D center, scrubber bottom
|
||||
const layout = document.createElement('div');
|
||||
layout.style.cssText = 'display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden';
|
||||
container.appendChild(layout);
|
||||
|
||||
// ── View header with explanation ──
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:16px;flex-shrink:0';
|
||||
header.innerHTML = `
|
||||
<div style="flex:1">
|
||||
<div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:2px">Coherence Field</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.4">
|
||||
Measures causal consistency across the event graph. High coherence (blue, flat) = events agree.
|
||||
Low coherence (red, raised peaks) = conflicting evidence or boundary pressure.
|
||||
Warning threshold at 0.80, critical at 0.70.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
layout.appendChild(header);
|
||||
|
||||
// ── Metric cards row ──
|
||||
const metricsRow = document.createElement('div');
|
||||
metricsRow.style.cssText = 'display:flex;gap:12px;padding:12px 20px;flex-shrink:0;flex-wrap:wrap';
|
||||
const metricDefs = [
|
||||
{ key: 'mean', label: 'MEAN COHERENCE', icon: '~' },
|
||||
{ key: 'min', label: 'MINIMUM', icon: 'v' },
|
||||
{ key: 'max', label: 'MAXIMUM', icon: '^' },
|
||||
{ key: 'violations', label: 'BELOW THRESHOLD', icon: '!' },
|
||||
];
|
||||
for (const m of metricDefs) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'metric-card';
|
||||
card.style.cssText = 'flex:1;min-width:140px';
|
||||
const valEl = document.createElement('div');
|
||||
valEl.className = 'metric-value';
|
||||
valEl.textContent = '--';
|
||||
const labelEl = document.createElement('div');
|
||||
labelEl.className = 'metric-label';
|
||||
labelEl.textContent = m.label;
|
||||
card.appendChild(labelEl);
|
||||
card.appendChild(valEl);
|
||||
metricsRow.appendChild(card);
|
||||
this.metricsEls[m.key] = valEl;
|
||||
}
|
||||
layout.appendChild(metricsRow);
|
||||
|
||||
// ── Main area: 3D + alerts sidebar ──
|
||||
const mainArea = document.createElement('div');
|
||||
mainArea.style.cssText = 'flex:1;display:flex;overflow:hidden;min-height:0';
|
||||
layout.appendChild(mainArea);
|
||||
|
||||
// Three.js canvas
|
||||
const canvasDiv = document.createElement('div');
|
||||
canvasDiv.className = 'three-container';
|
||||
canvasDiv.style.flex = '1';
|
||||
mainArea.appendChild(canvasDiv);
|
||||
|
||||
// HUD overlay for hover info
|
||||
this.hud = document.createElement('div');
|
||||
this.hud.style.cssText = `
|
||||
position:absolute;top:12px;left:12px;
|
||||
padding:8px 12px;background:rgba(11,15,20,0.92);
|
||||
border:1px solid var(--border);border-radius:4px;
|
||||
font-family:var(--font-mono);font-size:11px;color:var(--text-secondary);
|
||||
pointer-events:none;display:none;z-index:10;line-height:1.6;
|
||||
`;
|
||||
canvasDiv.appendChild(this.hud);
|
||||
|
||||
// Color legend overlay
|
||||
const legend = document.createElement('div');
|
||||
legend.style.cssText = `
|
||||
position:absolute;bottom:12px;right:12px;
|
||||
padding:8px 12px;background:rgba(11,15,20,0.9);
|
||||
border:1px solid var(--border);border-radius:4px;
|
||||
font-family:var(--font-mono);font-size:10px;color:var(--text-secondary);
|
||||
z-index:10;display:flex;flex-direction:column;gap:4px;
|
||||
`;
|
||||
legend.innerHTML = `
|
||||
<div style="font-size:9px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:2px">Coherence Scale</div>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<div style="width:60px;height:6px;border-radius:3px;background:linear-gradient(to right,#FF4D4D,#FFB020,#00E5FF,#0044AA)"></div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;width:60px">
|
||||
<span>0.6</span><span>0.8</span><span>1.0</span>
|
||||
</div>
|
||||
<div style="margin-top:4px;display:flex;flex-direction:column;gap:2px">
|
||||
<div style="display:flex;align-items:center;gap:4px"><span style="width:6px;height:6px;border-radius:50%;background:#FFB020;display:inline-block"></span> Warning <0.80</div>
|
||||
<div style="display:flex;align-items:center;gap:4px"><span style="width:6px;height:6px;border-radius:50%;background:#FF4D4D;display:inline-block"></span> Critical <0.70</div>
|
||||
</div>
|
||||
`;
|
||||
canvasDiv.appendChild(legend);
|
||||
|
||||
// Interaction hint
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = `
|
||||
position:absolute;bottom:12px;left:12px;
|
||||
font-size:10px;color:var(--text-muted);font-family:var(--font-mono);
|
||||
z-index:10;pointer-events:none;
|
||||
`;
|
||||
hint.textContent = 'Drag to rotate, scroll to zoom, hover for values';
|
||||
canvasDiv.appendChild(hint);
|
||||
|
||||
// ── Alerts sidebar ──
|
||||
const alertsSidebar = document.createElement('div');
|
||||
alertsSidebar.style.cssText = 'width:240px;background:var(--bg-panel);border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;flex-shrink:0';
|
||||
const alertsHeader = document.createElement('div');
|
||||
alertsHeader.className = 'panel-header';
|
||||
alertsHeader.textContent = 'Active Alerts';
|
||||
alertsSidebar.appendChild(alertsHeader);
|
||||
this.alertList = document.createElement('div');
|
||||
this.alertList.style.cssText = 'flex:1;overflow-y:auto;padding:4px 0';
|
||||
alertsSidebar.appendChild(this.alertList);
|
||||
mainArea.appendChild(alertsSidebar);
|
||||
|
||||
// ── Epoch scrubber ──
|
||||
const scrubberDiv = document.createElement('div');
|
||||
scrubberDiv.className = 'time-scrubber';
|
||||
scrubberDiv.style.flexShrink = '0';
|
||||
const scrubLabel = document.createElement('span');
|
||||
scrubLabel.className = 'time-scrubber-title';
|
||||
scrubLabel.textContent = 'Epoch';
|
||||
scrubberDiv.appendChild(scrubLabel);
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.className = 'time-scrubber-range';
|
||||
slider.min = '0';
|
||||
slider.max = '100';
|
||||
slider.value = '0';
|
||||
scrubberDiv.appendChild(slider);
|
||||
const scrubVal = document.createElement('span');
|
||||
scrubVal.className = 'time-scrubber-label';
|
||||
scrubVal.textContent = 'E0';
|
||||
scrubberDiv.appendChild(scrubVal);
|
||||
slider.addEventListener('input', () => {
|
||||
const epoch = Number(slider.value);
|
||||
scrubVal.textContent = `E${epoch}`;
|
||||
this.currentEpoch = epoch;
|
||||
this.loadData(epoch);
|
||||
});
|
||||
layout.appendChild(scrubberDiv);
|
||||
|
||||
// ── Three.js setup ──
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x0B0F14);
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
|
||||
this.camera.position.set(4, 7, 10);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
canvasDiv.appendChild(this.renderer.domElement);
|
||||
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.1;
|
||||
this.controls.maxPolarAngle = Math.PI * 0.45;
|
||||
this.controls.minDistance = 4;
|
||||
this.controls.maxDistance = 30;
|
||||
|
||||
// Lighting for phong material
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, 0.4));
|
||||
const dirLight = new THREE.DirectionalLight(0xCCDDFF, 0.6);
|
||||
dirLight.position.set(5, 10, 5);
|
||||
this.scene.add(dirLight);
|
||||
const fillLight = new THREE.DirectionalLight(0x4488AA, 0.3);
|
||||
fillLight.position.set(-5, 3, -5);
|
||||
this.scene.add(fillLight);
|
||||
|
||||
this.surface = new CoherenceSurface(this.scene, this.gridSize, this.gridSize);
|
||||
|
||||
// Mouse hover for value readout
|
||||
canvasDiv.addEventListener('mousemove', this.onMouseMove);
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', this.resize);
|
||||
this.loadData(0);
|
||||
this.loadAlerts();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
private onMouseMove = (event: MouseEvent): void => {
|
||||
if (!this.renderer || !this.camera || !this.hud) return;
|
||||
const rect = this.renderer.domElement.getBoundingClientRect();
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||||
const meshes = this.scene?.children.filter((c) => c instanceof THREE.Mesh) ?? [];
|
||||
const intersects = this.raycaster.intersectObjects(meshes);
|
||||
|
||||
if (intersects.length > 0 && this.currentValues.length > 0) {
|
||||
const hit = intersects[0];
|
||||
const point = hit.point;
|
||||
// Map world coords back to grid cell
|
||||
const gx = Math.round(((point.x + 5) / 10) * (this.gridSize - 1));
|
||||
const gz = Math.round(((point.z + 5) / 10) * (this.gridSize - 1));
|
||||
if (gx >= 0 && gx < this.gridSize && gz >= 0 && gz < this.gridSize) {
|
||||
const idx = gz * this.gridSize + gx;
|
||||
const v = this.currentValues[idx];
|
||||
if (v !== undefined) {
|
||||
const status = v >= 0.85 ? 'STABLE' : v >= 0.80 ? 'NOMINAL' : v >= 0.70 ? 'WARNING' : 'CRITICAL';
|
||||
const color = v >= 0.85 ? 'var(--accent)' : v >= 0.80 ? 'var(--text-primary)' : v >= 0.70 ? 'var(--warning)' : 'var(--critical)';
|
||||
this.hud.style.display = 'block';
|
||||
this.hud.innerHTML = `
|
||||
<div style="color:var(--text-muted)">Sector (${gx}, ${gz})</div>
|
||||
<div style="font-size:16px;font-weight:600;color:${color}">${v.toFixed(3)}</div>
|
||||
<div style="color:${color};font-size:10px">${status}</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.hud) this.hud.style.display = 'none';
|
||||
};
|
||||
|
||||
private updateMetrics(values: number[]): void {
|
||||
const stats = computeStats(values);
|
||||
const setMetric = (key: string, val: string, cls?: string) => {
|
||||
const el = this.metricsEls[key];
|
||||
if (el) {
|
||||
el.textContent = val;
|
||||
el.className = 'metric-value' + (cls ? ` ${cls}` : '');
|
||||
}
|
||||
};
|
||||
setMetric('mean', stats.mean.toFixed(3), stats.mean >= 0.85 ? 'accent' : stats.mean >= 0.80 ? '' : 'warning');
|
||||
setMetric('min', stats.min.toFixed(3), stats.min >= 0.80 ? 'success' : stats.min >= 0.70 ? 'warning' : 'critical');
|
||||
setMetric('max', stats.max.toFixed(3), 'accent');
|
||||
setMetric('violations', `${stats.violations}`, stats.violations === 0 ? 'success' : stats.violations < 100 ? 'warning' : 'critical');
|
||||
}
|
||||
|
||||
private async loadAlerts(): Promise<void> {
|
||||
if (!this.alertList) return;
|
||||
try {
|
||||
const alerts = await fetchBoundaryAlerts();
|
||||
this.renderAlerts(alerts);
|
||||
} catch {
|
||||
this.renderAlerts([
|
||||
{ target_id: '7G', epoch: 7, pressure: 0.74, message: 'Coherence drop in sector 7G (0.74)' },
|
||||
{ target_id: '3A', epoch: 5, pressure: 0.62, message: 'Witness chain gap in sector 3A' },
|
||||
{ target_id: 'global', epoch: 7, pressure: 0.79, message: 'Boundary expansion +14.5%' },
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private renderAlerts(alerts: BoundaryAlert[]): void {
|
||||
if (!this.alertList) return;
|
||||
this.alertList.innerHTML = '';
|
||||
if (alerts.length === 0) {
|
||||
this.alertList.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:11px;text-align:center">No active alerts</div>';
|
||||
return;
|
||||
}
|
||||
for (const a of alerts) {
|
||||
const severity = a.pressure < 0.70 ? 'critical' : a.pressure < 0.80 ? 'warning' : 'success';
|
||||
const item = document.createElement('div');
|
||||
item.className = 'alert-item';
|
||||
item.innerHTML = `
|
||||
<div class="alert-dot ${severity}"></div>
|
||||
<div style="flex:1">
|
||||
<div style="font-size:11px;color:var(--text-primary);margin-bottom:2px">${a.message}</div>
|
||||
<div style="font-size:10px;color:var(--text-muted);font-family:var(--font-mono)">Sector ${a.target_id} | Coherence: ${a.pressure.toFixed(2)}</div>
|
||||
</div>
|
||||
`;
|
||||
this.alertList.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadData(epoch: number): Promise<void> {
|
||||
if (!this.surface) return;
|
||||
try {
|
||||
const data = await fetchCoherence('default', epoch);
|
||||
this.currentValues = data.map((d) => d.value);
|
||||
this.surface.setValues(this.currentValues);
|
||||
this.updateMetrics(this.currentValues);
|
||||
} catch {
|
||||
this.currentValues = generateDemoValues(this.gridSize, epoch);
|
||||
this.surface.setValues(this.currentValues);
|
||||
this.updateMetrics(this.currentValues);
|
||||
}
|
||||
}
|
||||
|
||||
private resize = (): void => {
|
||||
if (!this.renderer || !this.camera || !this.container) return;
|
||||
const canvas = this.renderer.domElement.parentElement;
|
||||
if (!canvas) return;
|
||||
const w = canvas.clientWidth;
|
||||
const h = canvas.clientHeight;
|
||||
this.renderer.setSize(w, h);
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
private animate = (): void => {
|
||||
this.animFrameId = requestAnimationFrame(this.animate);
|
||||
this.controls?.update();
|
||||
if (this.renderer && this.scene && this.camera) {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
};
|
||||
|
||||
unmount(): void {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
cancelAnimationFrame(this.animFrameId);
|
||||
this.surface?.dispose();
|
||||
this.controls?.dispose();
|
||||
this.renderer?.dispose();
|
||||
this.surface = null;
|
||||
this.controls = null;
|
||||
this.renderer = null;
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.container = null;
|
||||
this.hud = null;
|
||||
this.alertList = null;
|
||||
this.metricsEls = {};
|
||||
}
|
||||
}
|
||||
540
vendor/ruvector/examples/rvf/dashboard/src/views/DiscoveryView.ts
vendored
Normal file
540
vendor/ruvector/examples/rvf/dashboard/src/views/DiscoveryView.ts
vendored
Normal file
@@ -0,0 +1,540 @@
|
||||
/**
|
||||
* Discovery View — New planet discovery pipeline with 3D solar system visualization.
|
||||
*
|
||||
* Processes real unconfirmed KOI/TOI candidates through the RVF pipeline
|
||||
* to identify the most Earth-like world awaiting confirmation.
|
||||
* Includes interactive Three.js 3D visualization of each candidate's
|
||||
* orbital system — host star, planet orbit, habitable zone.
|
||||
*/
|
||||
|
||||
import { PlanetSystem3D, PlanetSystemParams } from '../three/PlanetSystem3D';
|
||||
|
||||
interface DiscoveryCandidate {
|
||||
id: string;
|
||||
catalog: string;
|
||||
status: string;
|
||||
raw_observations: Record<string, number>;
|
||||
pipeline_derived: {
|
||||
radius_earth: number;
|
||||
semi_major_axis_au: number;
|
||||
eq_temp_k: number;
|
||||
hz_member: boolean;
|
||||
esi_score: number;
|
||||
radius_similarity: number;
|
||||
temperature_similarity: number;
|
||||
};
|
||||
analysis: string;
|
||||
confirmation_needs: string[];
|
||||
significance: string;
|
||||
discovery_rank: number;
|
||||
}
|
||||
|
||||
interface DiscoveryData {
|
||||
mission: string;
|
||||
pipeline_stages: Array<{ stage: string; name: string; description: string }>;
|
||||
candidates: DiscoveryCandidate[];
|
||||
discovery: {
|
||||
top_candidate: string;
|
||||
esi_score: number;
|
||||
comparison: Record<string, string>;
|
||||
why_not_confirmed: string;
|
||||
what_confirmation_requires: string[];
|
||||
pipeline_witness_chain: Array<{ witness: string; measurement: string; confidence: number }>;
|
||||
};
|
||||
data_source: string;
|
||||
references: string[];
|
||||
}
|
||||
|
||||
export class DiscoveryView {
|
||||
private container: HTMLElement | null = null;
|
||||
private data: DiscoveryData | null = null;
|
||||
private pipelineEl: HTMLElement | null = null;
|
||||
private candidatesEl: HTMLElement | null = null;
|
||||
private discoveryEl: HTMLElement | null = null;
|
||||
private running = false;
|
||||
private currentStage = -1;
|
||||
|
||||
// 3D visualization
|
||||
private planet3d: PlanetSystem3D | null = null;
|
||||
private planet3dContainer: HTMLElement | null = null;
|
||||
private planet3dInfoEl: HTMLElement | null = null;
|
||||
private controlsEl: HTMLElement | null = null;
|
||||
private vizPanel: HTMLElement | null = null;
|
||||
private selectedCardEl: HTMLElement | null = null;
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'display:flex;flex-direction:column;width:100%;height:100%;overflow:auto';
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// Header
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'padding:16px 20px;border-bottom:1px solid var(--border);flex-shrink:0';
|
||||
header.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<div style="font-size:16px;font-weight:700;color:var(--text-primary)">New Planet Discovery</div>
|
||||
<span class="score-badge score-high" style="font-size:9px;padding:2px 8px">LIVE PIPELINE</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-secondary);line-height:1.7;max-width:900px">
|
||||
The RVF pipeline processes <strong>real unconfirmed candidates</strong> from the Kepler Objects of Interest (KOI) catalog.
|
||||
These are stars with detected transit signals that have not yet been confirmed as planets.
|
||||
The pipeline derives physical properties from raw photometry and ranks candidates by Earth Similarity Index.
|
||||
<strong>Click any candidate</strong> to view its 3D orbital system.
|
||||
</div>
|
||||
`;
|
||||
wrapper.appendChild(header);
|
||||
|
||||
// Pipeline stages
|
||||
this.pipelineEl = document.createElement('div');
|
||||
this.pipelineEl.style.cssText = 'padding:16px 20px;border-bottom:1px solid var(--border)';
|
||||
wrapper.appendChild(this.pipelineEl);
|
||||
|
||||
// Run button
|
||||
const controls = document.createElement('div');
|
||||
controls.style.cssText = 'padding:12px 20px;flex-shrink:0';
|
||||
const runBtn = document.createElement('button');
|
||||
runBtn.textContent = 'Run Discovery Pipeline';
|
||||
runBtn.style.cssText =
|
||||
'padding:10px 24px;border:none;border-radius:6px;background:var(--accent);' +
|
||||
'color:#0B0F14;font-size:13px;font-weight:700;cursor:pointer;letter-spacing:0.3px';
|
||||
runBtn.addEventListener('click', () => this.runPipeline());
|
||||
controls.appendChild(runBtn);
|
||||
wrapper.appendChild(controls);
|
||||
|
||||
// 3D Visualization panel (hidden until candidate selected)
|
||||
this.vizPanel = document.createElement('div');
|
||||
this.vizPanel.style.cssText =
|
||||
'padding:0 20px 16px;display:none';
|
||||
wrapper.appendChild(this.vizPanel);
|
||||
|
||||
const vizInner = document.createElement('div');
|
||||
vizInner.style.cssText =
|
||||
'display:grid;grid-template-columns:1fr 260px;gap:0;' +
|
||||
'background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;overflow:hidden';
|
||||
this.vizPanel.appendChild(vizInner);
|
||||
|
||||
// 3D viewport (larger)
|
||||
this.planet3dContainer = document.createElement('div');
|
||||
this.planet3dContainer.style.cssText =
|
||||
'position:relative;min-height:420px;background:#020408';
|
||||
vizInner.appendChild(this.planet3dContainer);
|
||||
|
||||
// Controls overlay (bottom-left of viewport)
|
||||
this.controlsEl = document.createElement('div');
|
||||
this.controlsEl.style.cssText =
|
||||
'position:absolute;bottom:10px;left:10px;display:flex;gap:6px;align-items:center;z-index:20;' +
|
||||
'background:rgba(2,4,8,0.8);border:1px solid rgba(30,38,48,0.6);border-radius:6px;padding:6px 10px';
|
||||
this.planet3dContainer.appendChild(this.controlsEl);
|
||||
|
||||
// Interaction hint (top-right)
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText =
|
||||
'position:absolute;top:8px;right:8px;font-size:9px;color:rgba(230,237,243,0.4);' +
|
||||
'z-index:20;pointer-events:none;text-align:right;line-height:1.6';
|
||||
hint.innerHTML = 'Drag to rotate<br>Scroll to zoom<br>Right-drag to pan';
|
||||
this.planet3dContainer.appendChild(hint);
|
||||
|
||||
// Info sidebar
|
||||
this.planet3dInfoEl = document.createElement('div');
|
||||
this.planet3dInfoEl.style.cssText =
|
||||
'padding:16px;overflow-y:auto;max-height:420px;font-size:11px;' +
|
||||
'color:var(--text-secondary);line-height:1.7;border-left:1px solid var(--border)';
|
||||
vizInner.appendChild(this.planet3dInfoEl);
|
||||
|
||||
// Candidates area
|
||||
this.candidatesEl = document.createElement('div');
|
||||
this.candidatesEl.style.cssText = 'padding:0 20px 16px';
|
||||
wrapper.appendChild(this.candidatesEl);
|
||||
|
||||
// Discovery result
|
||||
this.discoveryEl = document.createElement('div');
|
||||
this.discoveryEl.style.cssText = 'padding:0 20px 24px;display:none';
|
||||
wrapper.appendChild(this.discoveryEl);
|
||||
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
private async loadData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/api/discover');
|
||||
this.data = (await response.json()) as DiscoveryData;
|
||||
} catch (err) {
|
||||
console.error('Discovery API error:', err);
|
||||
return;
|
||||
}
|
||||
this.renderPipelineStages();
|
||||
}
|
||||
|
||||
private renderPipelineStages(): void {
|
||||
if (!this.pipelineEl || !this.data) return;
|
||||
const stages = this.data.pipeline_stages;
|
||||
this.pipelineEl.innerHTML = `
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">Pipeline Stages</div>
|
||||
<div style="display:flex;gap:4px;align-items:center;flex-wrap:wrap">
|
||||
${stages.map((s, i) => `
|
||||
<div id="stage-${i}" style="padding:6px 14px;border-radius:4px;background:var(--bg-surface);border:1px solid var(--border);transition:all 0.3s">
|
||||
<div style="font-size:10px;font-weight:700;color:var(--text-muted)">${s.stage}</div>
|
||||
<div style="font-size:9px;color:var(--text-muted)">${s.name}</div>
|
||||
</div>
|
||||
${i < stages.length - 1 ? '<span style="color:var(--text-muted)">→</span>' : ''}
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async runPipeline(): Promise<void> {
|
||||
if (this.running || !this.data) return;
|
||||
this.running = true;
|
||||
this.currentStage = -1;
|
||||
|
||||
// Clear previous results
|
||||
if (this.candidatesEl) this.candidatesEl.innerHTML = '';
|
||||
if (this.discoveryEl) this.discoveryEl.style.display = 'none';
|
||||
if (this.vizPanel) this.vizPanel.style.display = 'none';
|
||||
this.selectedCardEl = null;
|
||||
|
||||
// Animate through pipeline stages
|
||||
for (let i = 0; i < this.data.pipeline_stages.length; i++) {
|
||||
this.currentStage = i;
|
||||
this.highlightStage(i);
|
||||
await this.sleep(600);
|
||||
}
|
||||
|
||||
// Show candidates one by one
|
||||
const sorted = [...this.data.candidates].sort((a, b) => a.discovery_rank - b.discovery_rank);
|
||||
for (const c of sorted) {
|
||||
await this.sleep(400);
|
||||
this.addCandidateCard(c);
|
||||
}
|
||||
|
||||
// Auto-show 3D for the top-ranked candidate
|
||||
if (sorted.length > 0) {
|
||||
this.show3D(sorted[0]);
|
||||
}
|
||||
|
||||
// Show discovery
|
||||
await this.sleep(800);
|
||||
this.showDiscovery();
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
private highlightStage(index: number): void {
|
||||
for (let i = 0; i < (this.data?.pipeline_stages.length ?? 0); i++) {
|
||||
const el = document.getElementById(`stage-${i}`);
|
||||
if (!el) continue;
|
||||
if (i < index) {
|
||||
el.style.background = 'rgba(46,204,113,0.15)';
|
||||
el.style.borderColor = 'rgba(46,204,113,0.4)';
|
||||
el.querySelector('div')!.style.color = 'var(--success)';
|
||||
} else if (i === index) {
|
||||
el.style.background = 'rgba(0,229,255,0.15)';
|
||||
el.style.borderColor = 'var(--accent)';
|
||||
el.querySelector('div')!.style.color = 'var(--accent)';
|
||||
} else {
|
||||
el.style.background = 'var(--bg-surface)';
|
||||
el.style.borderColor = 'var(--border)';
|
||||
el.querySelector('div')!.style.color = 'var(--text-muted)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addCandidateCard(c: DiscoveryCandidate): void {
|
||||
if (!this.candidatesEl) return;
|
||||
|
||||
const esiClass = c.pipeline_derived.esi_score >= 0.9 ? 'score-high' : c.pipeline_derived.esi_score >= 0.8 ? 'score-medium' : 'score-low';
|
||||
const isTop = c.discovery_rank === 1;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.style.cssText = `
|
||||
padding:12px 16px;margin-bottom:8px;border-radius:6px;cursor:pointer;
|
||||
background:${isTop ? 'rgba(0,229,255,0.08)' : 'var(--bg-surface)'};
|
||||
border:1px solid ${isTop ? 'var(--accent)' : 'var(--border)'};
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
`;
|
||||
card.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text-primary)">${c.id}</span>
|
||||
<span class="score-badge ${esiClass}" style="font-size:10px">ESI ${c.pipeline_derived.esi_score.toFixed(2)}</span>
|
||||
<span style="font-size:9px;color:var(--text-muted)">${c.catalog}</span>
|
||||
${isTop ? '<span class="score-badge score-high" style="font-size:8px;background:rgba(0,229,255,0.2);color:var(--accent);border-color:var(--accent)">TOP DISCOVERY</span>' : ''}
|
||||
<span style="font-size:9px;color:var(--text-muted);margin-left:4px" title="Click to view 3D system">🌐 View 3D</span>
|
||||
<span style="margin-left:auto;font-size:10px;color:var(--text-muted)">
|
||||
R=${c.pipeline_derived.radius_earth.toFixed(2)} R⊕ | T=${c.pipeline_derived.eq_temp_k}K |
|
||||
HZ: ${c.pipeline_derived.hz_member ? '<span style="color:var(--success)">YES</span>' : '<span style="color:var(--critical)">NO</span>'}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.5">${c.analysis}</div>
|
||||
`;
|
||||
|
||||
card.addEventListener('click', () => {
|
||||
this.show3D(c);
|
||||
// Highlight selected card
|
||||
if (this.selectedCardEl) {
|
||||
this.selectedCardEl.style.borderColor = this.selectedCardEl.dataset.isTop === '1' ? 'var(--accent)' : 'var(--border)';
|
||||
this.selectedCardEl.style.boxShadow = 'none';
|
||||
}
|
||||
card.style.borderColor = 'var(--accent)';
|
||||
card.style.boxShadow = '0 0 8px rgba(0,229,255,0.2)';
|
||||
this.selectedCardEl = card;
|
||||
});
|
||||
card.dataset.isTop = isTop ? '1' : '0';
|
||||
|
||||
this.candidatesEl.appendChild(card);
|
||||
}
|
||||
|
||||
private show3D(c: DiscoveryCandidate): void {
|
||||
if (!this.vizPanel || !this.planet3dContainer || !this.planet3dInfoEl) return;
|
||||
|
||||
this.vizPanel.style.display = '';
|
||||
|
||||
// Destroy previous 3D if exists
|
||||
if (this.planet3d) {
|
||||
this.planet3d.destroy();
|
||||
this.planet3d = null;
|
||||
}
|
||||
|
||||
// Create new 3D system
|
||||
this.planet3d = new PlanetSystem3D(this.planet3dContainer);
|
||||
const params: PlanetSystemParams = {
|
||||
label: c.id,
|
||||
radiusEarth: c.pipeline_derived.radius_earth,
|
||||
semiMajorAxisAU: c.pipeline_derived.semi_major_axis_au,
|
||||
eqTempK: c.pipeline_derived.eq_temp_k,
|
||||
stellarTempK: c.raw_observations['stellar_temp_k'] ?? 5500,
|
||||
stellarRadiusSolar: c.raw_observations['stellar_radius_solar'] ?? 1.0,
|
||||
periodDays: c.raw_observations['period_days'] ?? 365,
|
||||
hzMember: c.pipeline_derived.hz_member,
|
||||
esiScore: c.pipeline_derived.esi_score,
|
||||
transitDepth: c.raw_observations['transit_depth'] ?? 0.001,
|
||||
};
|
||||
this.planet3d.update(params);
|
||||
|
||||
// Build controls overlay
|
||||
this.buildControls();
|
||||
|
||||
// Update info sidebar
|
||||
const starType = this.getSpectralType(params.stellarTempK);
|
||||
const tempLabel = this.getTempLabel(params.eqTempK);
|
||||
|
||||
this.planet3dInfoEl.innerHTML = `
|
||||
<div style="font-size:14px;font-weight:700;color:var(--text-primary);margin-bottom:10px">${c.id}</div>
|
||||
<div style="margin-bottom:12px">
|
||||
<span class="score-badge ${c.pipeline_derived.esi_score >= 0.9 ? 'score-high' : c.pipeline_derived.esi_score >= 0.8 ? 'score-medium' : 'score-low'}"
|
||||
style="font-size:11px;padding:3px 8px">ESI ${c.pipeline_derived.esi_score.toFixed(2)}</span>
|
||||
<span style="margin-left:6px;font-size:10px;color:${c.pipeline_derived.hz_member ? 'var(--success)' : 'var(--critical)'}">
|
||||
${c.pipeline_derived.hz_member ? 'Habitable Zone' : 'Outside HZ'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="font-size:10px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px">Planet</div>
|
||||
<div style="padding-left:8px;border-left:2px solid var(--accent);margin-bottom:12px">
|
||||
<div>Radius: <span style="color:var(--accent)">${params.radiusEarth.toFixed(2)} R⊕</span></div>
|
||||
<div>Temperature: <span style="color:var(--accent)">${params.eqTempK} K</span> <span style="font-size:9px;color:var(--text-muted)">(${tempLabel})</span></div>
|
||||
<div>Orbit: <span style="color:var(--accent)">${params.semiMajorAxisAU.toFixed(3)} AU</span></div>
|
||||
<div>Period: <span style="color:var(--accent)">${params.periodDays.toFixed(1)} days</span></div>
|
||||
<div>Transit depth: <span style="color:var(--accent)">${(params.transitDepth * 1e6).toFixed(0)} ppm</span></div>
|
||||
</div>
|
||||
|
||||
<div style="font-size:10px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px">Host Star</div>
|
||||
<div style="padding-left:8px;border-left:2px solid #ffd2a1;margin-bottom:12px">
|
||||
<div>Type: <span style="color:#ffd2a1">${starType}</span></div>
|
||||
<div>T<sub>eff</sub>: <span style="color:#ffd2a1">${params.stellarTempK} K</span></div>
|
||||
<div>Radius: <span style="color:#ffd2a1">${params.stellarRadiusSolar.toFixed(3)} R☉</span></div>
|
||||
</div>
|
||||
|
||||
<div style="font-size:10px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px">ESI Breakdown</div>
|
||||
<div style="padding-left:8px;border-left:2px solid var(--success);margin-bottom:12px">
|
||||
<div>Radius similarity: <span style="color:var(--success)">${(c.pipeline_derived.radius_similarity * 100).toFixed(0)}%</span></div>
|
||||
<div>Temp similarity: <span style="color:var(--success)">${(c.pipeline_derived.temperature_similarity * 100).toFixed(0)}%</span></div>
|
||||
</div>
|
||||
|
||||
<div style="font-size:10px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px">Comparison to Earth</div>
|
||||
<div style="font-size:10px;line-height:1.8">
|
||||
${this.earthComparison(params)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Scroll viz into view
|
||||
this.vizPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
private buildControls(): void {
|
||||
if (!this.controlsEl || !this.planet3d) return;
|
||||
this.controlsEl.innerHTML = '';
|
||||
|
||||
const btnStyle =
|
||||
'border:1px solid rgba(30,38,48,0.8);border-radius:4px;background:rgba(11,15,20,0.9);' +
|
||||
'color:var(--text-secondary);font-size:10px;padding:4px 8px;cursor:pointer;' +
|
||||
'font-family:var(--font-mono);transition:color 0.15s,border-color 0.15s';
|
||||
const activeBtnStyle = btnStyle.replace('var(--text-secondary)', 'var(--accent)').replace('rgba(30,38,48,0.8)', 'var(--accent)');
|
||||
|
||||
// Speed label
|
||||
const speedLabel = document.createElement('span');
|
||||
speedLabel.style.cssText = 'font-size:9px;color:var(--text-muted);font-family:var(--font-mono)';
|
||||
speedLabel.textContent = 'Speed:';
|
||||
this.controlsEl.appendChild(speedLabel);
|
||||
|
||||
// Speed slider
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.min = '0.1';
|
||||
slider.max = '5';
|
||||
slider.step = '0.1';
|
||||
slider.value = '1';
|
||||
slider.style.cssText = 'width:70px;height:4px;accent-color:var(--accent);cursor:pointer';
|
||||
slider.addEventListener('input', () => {
|
||||
const v = parseFloat(slider.value);
|
||||
this.planet3d?.setSpeed(v);
|
||||
speedVal.textContent = `${v.toFixed(1)}x`;
|
||||
});
|
||||
this.controlsEl.appendChild(slider);
|
||||
|
||||
const speedVal = document.createElement('span');
|
||||
speedVal.style.cssText = 'font-size:9px;color:var(--accent);min-width:24px;font-family:var(--font-mono)';
|
||||
speedVal.textContent = '1.0x';
|
||||
this.controlsEl.appendChild(speedVal);
|
||||
|
||||
// Separator
|
||||
const sep = document.createElement('span');
|
||||
sep.style.cssText = 'width:1px;height:14px;background:rgba(30,38,48,0.6)';
|
||||
this.controlsEl.appendChild(sep);
|
||||
|
||||
// Auto-rotate toggle
|
||||
const autoBtn = document.createElement('button');
|
||||
autoBtn.style.cssText = activeBtnStyle;
|
||||
autoBtn.textContent = 'Auto';
|
||||
autoBtn.title = 'Toggle auto-rotate camera';
|
||||
autoBtn.addEventListener('click', () => {
|
||||
this.planet3d?.toggleAutoRotate();
|
||||
const active = this.planet3d?.getAutoRotate() ?? false;
|
||||
autoBtn.style.cssText = active ? activeBtnStyle : btnStyle;
|
||||
});
|
||||
this.controlsEl.appendChild(autoBtn);
|
||||
|
||||
// Reset view button
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.style.cssText = btnStyle;
|
||||
resetBtn.textContent = 'Reset';
|
||||
resetBtn.title = 'Reset camera to default position';
|
||||
resetBtn.addEventListener('click', () => {
|
||||
this.planet3d?.resetCamera();
|
||||
autoBtn.style.cssText = activeBtnStyle;
|
||||
slider.value = '1';
|
||||
speedVal.textContent = '1.0x';
|
||||
this.planet3d?.setSpeed(1);
|
||||
});
|
||||
this.controlsEl.appendChild(resetBtn);
|
||||
}
|
||||
|
||||
private getSpectralType(teff: number): string {
|
||||
if (teff > 7500) return `A-type (${teff} K)`;
|
||||
if (teff > 6000) return `F-type (${teff} K)`;
|
||||
if (teff > 5200) return `G-type (${teff} K) — Sun-like`;
|
||||
if (teff > 3700) return `K-type (${teff} K)`;
|
||||
return `M-type (${teff} K)`;
|
||||
}
|
||||
|
||||
private getTempLabel(eqTempK: number): string {
|
||||
if (eqTempK < 180) return 'frozen';
|
||||
if (eqTempK < 240) return 'cold';
|
||||
if (eqTempK < 280) return 'temperate';
|
||||
if (eqTempK < 330) return 'warm';
|
||||
return 'hot';
|
||||
}
|
||||
|
||||
private earthComparison(p: PlanetSystemParams): string {
|
||||
const rRatio = p.radiusEarth;
|
||||
const tRatio = p.eqTempK / 255; // Earth's effective temp ~255K
|
||||
const aRatio = p.semiMajorAxisAU; // Earth = 1 AU
|
||||
const pRatio = p.periodDays / 365.25;
|
||||
|
||||
const fmt = (v: number, unit: string) => {
|
||||
if (v > 0.95 && v < 1.05) return `<span style="color:var(--success)">~Earth (${v.toFixed(2)}${unit})</span>`;
|
||||
if (v > 1) return `<span style="color:var(--accent)">${v.toFixed(2)}x Earth</span>`;
|
||||
return `<span style="color:var(--accent)">${v.toFixed(2)}x Earth</span>`;
|
||||
};
|
||||
|
||||
return `
|
||||
<div>Radius: ${fmt(rRatio, 'x')}</div>
|
||||
<div>Temperature: ${fmt(tRatio, 'x')}</div>
|
||||
<div>Orbit: ${fmt(aRatio, ' AU')}</div>
|
||||
<div>Year: ${fmt(pRatio, 'x')}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private showDiscovery(): void {
|
||||
if (!this.discoveryEl || !this.data) return;
|
||||
const d = this.data.discovery;
|
||||
this.discoveryEl.style.display = '';
|
||||
|
||||
this.discoveryEl.innerHTML = `
|
||||
<div style="padding:20px;background:rgba(0,229,255,0.06);border:2px solid var(--accent);border-radius:10px">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div style="font-size:18px;font-weight:800;color:var(--accent)">DISCOVERY: ${d.top_candidate}</div>
|
||||
<span class="score-badge score-high" style="font-size:12px;padding:3px 10px">ESI ${d.esi_score}</span>
|
||||
<span style="font-size:10px;color:var(--success);font-weight:600">MOST EARTH-LIKE CANDIDATE IN KEPLER CATALOG</span>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px">
|
||||
<div>
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:6px">Comparison to Known Worlds</div>
|
||||
${Object.entries(d.comparison).map(([k, v]) => `
|
||||
<div style="font-size:11px;color:var(--text-secondary);margin-bottom:4px;padding-left:8px;border-left:2px solid var(--accent)">
|
||||
<strong>${k.replace('vs_', 'vs ')}</strong>: ${v}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:6px">Why Not Yet Confirmed</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.6">${d.why_not_confirmed}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:16px">
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:6px">Pipeline Witness Chain</div>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
${d.pipeline_witness_chain.map(w => `
|
||||
<div style="padding:6px 10px;background:var(--bg-panel);border:1px solid var(--border);border-radius:4px;font-size:10px">
|
||||
<div style="color:var(--accent);font-weight:600">${w.witness}</div>
|
||||
<div style="color:var(--text-secondary)">${w.measurement}</div>
|
||||
<div style="color:${w.confidence > 0.9 ? 'var(--success)' : 'var(--warning)'}">${(w.confidence * 100).toFixed(0)}% conf.</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:6px">Steps to Confirmation</div>
|
||||
${d.what_confirmation_requires.map(s => `
|
||||
<div style="font-size:11px;color:var(--text-secondary);margin-bottom:3px;padding-left:8px">${s}</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
this.running = false;
|
||||
if (this.planet3d) {
|
||||
this.planet3d.destroy();
|
||||
this.planet3d = null;
|
||||
}
|
||||
this.container = null;
|
||||
this.pipelineEl = null;
|
||||
this.candidatesEl = null;
|
||||
this.discoveryEl = null;
|
||||
this.vizPanel = null;
|
||||
this.planet3dContainer = null;
|
||||
this.planet3dInfoEl = null;
|
||||
this.controlsEl = null;
|
||||
this.selectedCardEl = null;
|
||||
this.data = null;
|
||||
}
|
||||
}
|
||||
576
vendor/ruvector/examples/rvf/dashboard/src/views/DocsView.ts
vendored
Normal file
576
vendor/ruvector/examples/rvf/dashboard/src/views/DocsView.ts
vendored
Normal file
@@ -0,0 +1,576 @@
|
||||
/**
|
||||
* DocsView — Documentation with sidebar navigation and scroll-spy.
|
||||
*/
|
||||
|
||||
interface Section {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
children?: { id: string; label: string }[];
|
||||
}
|
||||
|
||||
const SECTIONS: Section[] = [
|
||||
{ id: 'overview', label: 'Overview', icon: '\u2302',
|
||||
children: [{ id: 'what-is-rvf', label: 'What is RVF?' }, { id: 'at-a-glance', label: 'At a Glance' }] },
|
||||
{ id: 'single-file', label: 'Single File', icon: '\u25A3',
|
||||
children: [{ id: 'segments', label: 'Segment Map' }, { id: 'why-one-file', label: 'Why One File?' }] },
|
||||
{ id: 'pipeline', label: 'Pipeline', icon: '\u25B6',
|
||||
children: [{ id: 'stage-ingest', label: 'Data Ingestion' }, { id: 'stage-process', label: 'Signal Processing' },
|
||||
{ id: 'stage-detect', label: 'Candidate Detection' }, { id: 'stage-score', label: 'Scoring' }, { id: 'stage-seal', label: 'Witness Sealing' }] },
|
||||
{ id: 'proof', label: 'Proof', icon: '\u2713',
|
||||
children: [{ id: 'witness-chain', label: 'Witness Chain' }, { id: 'reproducible', label: 'Reproducible' },
|
||||
{ id: 'acceptance', label: 'Acceptance Test' }, { id: 'blind', label: 'Blind Testing' }] },
|
||||
{ id: 'unique', label: 'Why Unique', icon: '\u2605' },
|
||||
{ id: 'capabilities', label: 'Views', icon: '\u25CE',
|
||||
children: [{ id: 'cap-atlas', label: 'Atlas Explorer' }, { id: 'cap-coherence', label: 'Coherence' },
|
||||
{ id: 'cap-boundaries', label: 'Boundaries' }, { id: 'cap-memory', label: 'Memory Tiers' },
|
||||
{ id: 'cap-planets', label: 'Planets' }, { id: 'cap-life', label: 'Life' },
|
||||
{ id: 'cap-witness', label: 'Witness Chain' }, { id: 'cap-solver', label: 'Solver' },
|
||||
{ id: 'cap-blind', label: 'Blind Test' }, { id: 'cap-discover', label: 'Discovery' },
|
||||
{ id: 'cap-dyson', label: 'Dyson Sphere' }, { id: 'cap-status', label: 'Status' }] },
|
||||
{ id: 'solver', label: 'Solver', icon: '\u2699',
|
||||
children: [{ id: 'thompson', label: 'Thompson Sampling' }, { id: 'auto-optimize', label: 'Auto-Optimize' }] },
|
||||
{ id: 'format', label: 'Format Spec', icon: '\u2630',
|
||||
children: [{ id: 'file-header', label: 'File Header' }, { id: 'seg-types', label: 'Segment Types' },
|
||||
{ id: 'witness-format', label: 'Witness Entry' }, { id: 'dashboard-seg', label: 'Dashboard Segment' }] },
|
||||
{ id: 'glossary', label: 'Glossary', icon: '\u2261' },
|
||||
];
|
||||
|
||||
export class DocsView {
|
||||
private container: HTMLElement | null = null;
|
||||
private contentEl: HTMLElement | null = null;
|
||||
private navLinks: Map<string, HTMLElement> = new Map();
|
||||
private scrollRaf = 0;
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'display:flex;width:100%;height:100%;overflow:hidden';
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// Left nav sidebar
|
||||
const nav = this.buildNav();
|
||||
wrapper.appendChild(nav);
|
||||
|
||||
// Right content area
|
||||
this.contentEl = document.createElement('div');
|
||||
this.contentEl.style.cssText = 'flex:1;overflow-y:auto;overflow-x:hidden;scroll-behavior:smooth;-webkit-overflow-scrolling:touch;min-width:0';
|
||||
wrapper.appendChild(this.contentEl);
|
||||
|
||||
const inner = document.createElement('div');
|
||||
inner.style.cssText = 'max-width:820px;margin:0 auto;padding:28px 32px 100px;line-height:1.7;color:var(--text-secondary);font-size:13px';
|
||||
this.contentEl.appendChild(inner);
|
||||
inner.innerHTML = this.buildContent();
|
||||
|
||||
// Scroll spy
|
||||
this.contentEl.addEventListener('scroll', this.onScroll);
|
||||
requestAnimationFrame(() => this.onScroll());
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
cancelAnimationFrame(this.scrollRaf);
|
||||
this.contentEl?.removeEventListener('scroll', this.onScroll);
|
||||
this.navLinks.clear();
|
||||
this.contentEl = null;
|
||||
this.container = null;
|
||||
}
|
||||
|
||||
/* ── Nav sidebar ── */
|
||||
|
||||
private buildNav(): HTMLElement {
|
||||
const nav = document.createElement('nav');
|
||||
nav.style.cssText = `
|
||||
width:220px;min-width:220px;background:var(--bg-panel);border-right:1px solid var(--border);
|
||||
overflow-y:auto;overflow-x:hidden;padding:16px 0;display:flex;flex-direction:column;
|
||||
-webkit-overflow-scrolling:touch;flex-shrink:0
|
||||
`;
|
||||
|
||||
// Title
|
||||
const title = document.createElement('div');
|
||||
title.style.cssText = 'padding:0 16px 14px;font-size:13px;font-weight:600;color:var(--text-primary);letter-spacing:0.3px;border-bottom:1px solid var(--border);margin-bottom:8px';
|
||||
title.textContent = 'Documentation';
|
||||
nav.appendChild(title);
|
||||
|
||||
for (const section of SECTIONS) {
|
||||
// Parent link
|
||||
const link = document.createElement('a');
|
||||
link.style.cssText = `
|
||||
display:flex;align-items:center;gap:8px;padding:7px 16px;
|
||||
font-size:12px;font-weight:600;color:var(--text-secondary);cursor:pointer;
|
||||
text-decoration:none;transition:color 0.15s,background 0.15s;border-left:2px solid transparent
|
||||
`;
|
||||
link.innerHTML = `<span style="font-size:11px;width:16px;text-align:center;opacity:0.6">${section.icon}</span> ${section.label}`;
|
||||
link.addEventListener('click', (e) => { e.preventDefault(); this.scrollTo(section.id); });
|
||||
link.addEventListener('mouseenter', () => { link.style.color = 'var(--text-primary)'; link.style.background = 'rgba(255,255,255,0.02)'; });
|
||||
link.addEventListener('mouseleave', () => {
|
||||
if (!link.classList.contains('doc-active')) { link.style.color = 'var(--text-secondary)'; link.style.background = ''; }
|
||||
});
|
||||
nav.appendChild(link);
|
||||
this.navLinks.set(section.id, link);
|
||||
|
||||
// Child links
|
||||
if (section.children) {
|
||||
for (const child of section.children) {
|
||||
const clink = document.createElement('a');
|
||||
clink.style.cssText = `
|
||||
display:block;padding:4px 16px 4px 40px;font-size:11px;color:var(--text-muted);
|
||||
cursor:pointer;text-decoration:none;transition:color 0.15s;border-left:2px solid transparent
|
||||
`;
|
||||
clink.textContent = child.label;
|
||||
clink.addEventListener('click', (e) => { e.preventDefault(); this.scrollTo(child.id); });
|
||||
clink.addEventListener('mouseenter', () => { clink.style.color = 'var(--text-secondary)'; });
|
||||
clink.addEventListener('mouseleave', () => {
|
||||
if (!clink.classList.contains('doc-active')) clink.style.color = 'var(--text-muted)';
|
||||
});
|
||||
nav.appendChild(clink);
|
||||
this.navLinks.set(child.id, clink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom spacer
|
||||
const spacer = document.createElement('div');
|
||||
spacer.style.cssText = 'flex:1;min-height:20px';
|
||||
nav.appendChild(spacer);
|
||||
|
||||
// Footer
|
||||
const footer = document.createElement('div');
|
||||
footer.style.cssText = 'padding:12px 16px;border-top:1px solid var(--border);font-size:9px;color:var(--text-muted);line-height:1.5';
|
||||
footer.innerHTML = 'Built with <span style="color:var(--accent)">RuVector</span><br>Rust + WASM + Three.js';
|
||||
nav.appendChild(footer);
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
private scrollTo(id: string): void {
|
||||
const el = this.contentEl?.querySelector(`#${id}`) as HTMLElement | null;
|
||||
if (el && this.contentEl) {
|
||||
this.contentEl.scrollTo({ top: el.offsetTop - 20, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Scroll spy ── */
|
||||
|
||||
private onScroll = (): void => {
|
||||
cancelAnimationFrame(this.scrollRaf);
|
||||
this.scrollRaf = requestAnimationFrame(() => {
|
||||
if (!this.contentEl) return;
|
||||
const scrollTop = this.contentEl.scrollTop + 60;
|
||||
|
||||
// Find which section is currently visible
|
||||
let activeId = '';
|
||||
const allIds = Array.from(this.navLinks.keys());
|
||||
for (const id of allIds) {
|
||||
const el = this.contentEl.querySelector(`#${id}`) as HTMLElement | null;
|
||||
if (el && el.offsetTop <= scrollTop) activeId = id;
|
||||
}
|
||||
|
||||
// Update nav highlights
|
||||
this.navLinks.forEach((link, id) => {
|
||||
const isActive = id === activeId;
|
||||
link.classList.toggle('doc-active', isActive);
|
||||
// Check if parent or child
|
||||
const isParent = SECTIONS.some(s => s.id === id);
|
||||
if (isActive) {
|
||||
link.style.color = 'var(--accent)';
|
||||
link.style.borderLeftColor = 'var(--accent)';
|
||||
link.style.background = isParent ? 'rgba(0,229,255,0.06)' : 'rgba(0,229,255,0.03)';
|
||||
} else {
|
||||
link.style.color = isParent ? 'var(--text-secondary)' : 'var(--text-muted)';
|
||||
link.style.borderLeftColor = 'transparent';
|
||||
link.style.background = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* ── Content builder ── */
|
||||
|
||||
private buildContent(): string {
|
||||
const S = {
|
||||
h1: 'font-size:26px;font-weight:300;color:var(--text-primary);letter-spacing:0.5px;margin-bottom:6px',
|
||||
h2: 'font-size:19px;font-weight:600;color:var(--text-primary);margin-top:48px;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)',
|
||||
h3: 'font-size:14px;font-weight:600;color:var(--accent);margin-top:28px;margin-bottom:8px',
|
||||
p: 'margin-bottom:14px',
|
||||
card: 'background:var(--bg-panel);border:1px solid var(--border);border-radius:6px;padding:14px 18px;margin-bottom:12px',
|
||||
code: 'font-family:var(--font-mono);font-size:11px;background:var(--bg-surface);border:1px solid var(--border);border-radius:4px;padding:12px 16px;display:block;margin:10px 0 14px;overflow-x:auto;line-height:1.6;color:var(--text-primary)',
|
||||
accent: 'color:var(--accent);font-weight:600',
|
||||
success: 'color:var(--success);font-weight:600',
|
||||
badge: 'display:inline-block;font-size:9px;font-weight:600;padding:2px 8px;border-radius:3px;margin-right:4px',
|
||||
inline: 'background:var(--bg-surface);padding:1px 6px;border-radius:3px;font-family:var(--font-mono);font-size:12px',
|
||||
};
|
||||
|
||||
return `
|
||||
<!-- ============ OVERVIEW ============ -->
|
||||
<div style="${S.h1}" id="overview">Causal Atlas Documentation</div>
|
||||
<div style="font-size:13px;color:var(--text-muted);margin-bottom:20px">
|
||||
A complete guide to the RVF scientific discovery platform.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="what-is-rvf">What is RVF?</div>
|
||||
<div style="${S.p}">
|
||||
<span style="${S.accent}">RVF (RuVector Format)</span> is a binary container that holds
|
||||
an entire scientific discovery pipeline — raw telescope data, analysis code,
|
||||
results, cryptographic proofs, and this interactive dashboard — in a
|
||||
<strong>single, self-contained file</strong>.
|
||||
</div>
|
||||
<div style="${S.p}">
|
||||
Think of it as a shipping container for science. Anyone who receives the file can
|
||||
independently verify every step of the analysis without external tools or databases.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="at-a-glance">At a Glance</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:16px">
|
||||
${this.statCard('File Format', 'Binary, segmented', S)}
|
||||
${this.statCard('Crypto', 'Ed25519 + SHAKE-256', S)}
|
||||
${this.statCard('Solver', 'WASM + Thompson Sampling', S)}
|
||||
${this.statCard('Dashboard', 'Three.js + D3', S)}
|
||||
${this.statCard('Server', 'Rust / Axum', S)}
|
||||
${this.statCard('Domains', 'Exoplanets, Dyson, Bio', S)}
|
||||
</div>
|
||||
|
||||
<!-- ============ SINGLE FILE ============ -->
|
||||
<div style="${S.h2}" id="single-file">One File Contains Everything</div>
|
||||
<div style="${S.p}">
|
||||
Traditional scientific data is scattered across files, servers, and packages.
|
||||
RVF packs everything into typed <strong>segments</strong> inside one binary file.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="segments">Segment Map</div>
|
||||
<div style="${S.card}">
|
||||
<div style="font-family:var(--font-mono);font-size:11px;line-height:2.2">
|
||||
${this.segRow('HEADER (64 B)', 'File magic, version, segment count', 'var(--text-muted)')}
|
||||
${this.segRow('DATA_SEG', 'Raw telescope observations (light curves, spectra)', '#FF6B9D')}
|
||||
${this.segRow('KERNEL_SEG', 'Processing algorithms for analysis', '#FFB020')}
|
||||
${this.segRow('EBPF_SEG', 'Fast in-kernel data filtering programs', '#9944FF')}
|
||||
${this.segRow('WASM_SEG', 'Self-learning solver (runs in any browser)', '#2ECC71')}
|
||||
${this.segRow('WITNESS_SEG', 'Cryptographic proof chain (Ed25519 signed)', 'var(--accent)')}
|
||||
${this.segRow('DASHBOARD_SEG', 'This interactive 3D dashboard (HTML/JS/CSS)', '#FF4D4D')}
|
||||
${this.segRow('SIGNATURE', 'Ed25519 signature over all segments', 'var(--text-muted)')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="why-one-file">Why One File?</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">
|
||||
<div style="${S.card}padding:10px 14px">
|
||||
<div style="font-size:11px;${S.accent}margin-bottom:3px">Portability</div>
|
||||
<div style="font-size:11px;line-height:1.5">Email it, USB drive, or static hosting. No server setup needed.</div>
|
||||
</div>
|
||||
<div style="${S.card}padding:10px 14px">
|
||||
<div style="font-size:11px;${S.accent}margin-bottom:3px">Reproducibility</div>
|
||||
<div style="font-size:11px;line-height:1.5">Code + data together means anyone can re-run the analysis.</div>
|
||||
</div>
|
||||
<div style="${S.card}padding:10px 14px">
|
||||
<div style="font-size:11px;${S.accent}margin-bottom:3px">Integrity</div>
|
||||
<div style="font-size:11px;line-height:1.5">Tampering with any segment breaks the signature chain.</div>
|
||||
</div>
|
||||
<div style="${S.card}padding:10px 14px">
|
||||
<div style="font-size:11px;${S.accent}margin-bottom:3px">Archival</div>
|
||||
<div style="font-size:11px;line-height:1.5">One file to store, back up, and cite. No link rot.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ PIPELINE ============ -->
|
||||
<div style="${S.h2}" id="pipeline">How the Pipeline Works</div>
|
||||
<div style="${S.p}">
|
||||
The pipeline transforms raw observations into verified discoveries through five stages.
|
||||
Each stage is recorded in the witness chain for full traceability.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="stage-ingest">1. Data Ingestion</div>
|
||||
<div style="${S.p}">
|
||||
Raw photometric data (brightness over time) is ingested from telescope archives.
|
||||
For exoplanet detection, this means <span style="${S.accent}">light curves</span> —
|
||||
graphs of stellar brightness that dip when a planet transits its star.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="stage-process">2. Signal Processing</div>
|
||||
<div style="${S.p}">
|
||||
Processing kernels clean the data: removing instrumental noise, correcting for stellar
|
||||
variability, and flagging periodic signals. The <span style="${S.accent}">eBPF programs</span>
|
||||
accelerate filtering at near-hardware speed.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="stage-detect">3. Candidate Detection</div>
|
||||
<div style="${S.p}">
|
||||
Cleaned signals are matched against known patterns. For exoplanets: periodic transit-shaped dips.
|
||||
For Dyson spheres: anomalous infrared excess. Each candidate gets derived parameters:
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:14px">
|
||||
<div style="${S.card}padding:10px 14px">
|
||||
<div style="font-size:10px;${S.accent}margin-bottom:3px">Exoplanets</div>
|
||||
<div style="font-size:11px">Radius, period, temperature, HZ membership, ESI score</div>
|
||||
</div>
|
||||
<div style="${S.card}padding:10px 14px">
|
||||
<div style="font-size:10px;color:#FFB020;font-weight:600;margin-bottom:3px">Dyson Candidates</div>
|
||||
<div style="font-size:11px">IR excess ratio, dimming pattern, partial coverage fraction</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="stage-score">4. Scoring & Ranking</div>
|
||||
<div style="${S.p}">
|
||||
Candidates are scored multi-dimensionally. The <span style="${S.accent}">WASM solver</span>
|
||||
uses Thompson Sampling to discover which analysis strategies work best for each difficulty
|
||||
level, continuously improving accuracy without human tuning.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="stage-seal">5. Witness Sealing</div>
|
||||
<div style="${S.p}">
|
||||
Every step is recorded in the <span style="${S.accent}">witness chain</span>: a SHAKE-256 hash
|
||||
of the data, a timestamp, and an Ed25519 signature. This creates an immutable,
|
||||
cryptographically verifiable audit trail.
|
||||
</div>
|
||||
|
||||
<!-- ============ PROOF ============ -->
|
||||
<div style="${S.h2}" id="proof">How Discoveries Are Proven</div>
|
||||
<div style="${S.p}">
|
||||
<strong>How do you know the results are real?</strong> RVF uses four layers of proof.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="witness-chain">Layer 1: Cryptographic Witness Chain</div>
|
||||
<div style="${S.card}">
|
||||
<div style="font-size:11px;line-height:1.9;margin-bottom:6px">
|
||||
Each processing step writes a witness entry containing:<br>
|
||||
• <strong>Step name</strong> — what operation was performed<br>
|
||||
• <strong>Input hash</strong> — SHAKE-256 of data going in<br>
|
||||
• <strong>Output hash</strong> — SHAKE-256 of data coming out<br>
|
||||
• <strong>Parent hash</strong> — links to previous entry (chain)<br>
|
||||
• <strong>Ed25519 signature</strong> — proves the entry is authentic
|
||||
</div>
|
||||
<div style="font-size:10px;color:var(--text-muted)">
|
||||
Each entry chains to the previous one. Altering any step breaks all subsequent signatures.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="reproducible">Layer 2: Reproducible Computation</div>
|
||||
<div style="${S.p}">
|
||||
The file contains the actual analysis code (WASM + eBPF) alongside raw data.
|
||||
Anyone can re-run the pipeline from scratch and verify identical results.
|
||||
No "trust us" — the math is in the file.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="acceptance">Layer 3: Acceptance Testing</div>
|
||||
<div style="${S.card}">
|
||||
<div style="font-size:11px;line-height:1.9">
|
||||
<span style="color:#FF4D4D;font-weight:600">Mode A (Heuristic)</span> — Can the solver achieve basic accuracy?<br>
|
||||
<span style="color:#FFB020;font-weight:600">Mode B (Compiler)</span> — Accuracy + computational cost reduction?<br>
|
||||
<span style="color:#2ECC71;font-weight:600">Mode C (Learned)</span> — Full: accuracy + cost + robustness + zero violations.
|
||||
</div>
|
||||
<div style="font-size:10px;color:var(--text-muted);margin-top:6px">
|
||||
All three modes must pass. The manifest is itself recorded in the witness chain.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="blind">Layer 4: Blind Testing</div>
|
||||
<div style="${S.p}">
|
||||
The Blind Test page runs the pipeline on unlabeled data, then compares against ground truth.
|
||||
This guards against overfitting — the pipeline must work on data it has never seen.
|
||||
</div>
|
||||
|
||||
<!-- ============ UNIQUE ============ -->
|
||||
<div style="${S.h2}" id="unique">What Makes This Unique</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px">
|
||||
${this.uniqueCard('Self-Contained', 'One file. No cloud, no databases, no external dependencies. The entire pipeline, visualization, and proof chain travel together.', S)}
|
||||
${this.uniqueCard('Cryptographically Verified', 'Every step is hashed and signed. Tampering with one part invalidates the entire chain. Mathematical proof, not just peer review.', S)}
|
||||
${this.uniqueCard('Self-Learning', 'The WASM solver improves over time using Thompson Sampling, discovering which strategies work for different data difficulties.', S)}
|
||||
${this.uniqueCard('Runs Anywhere', 'WASM solver + HTML dashboard + Rust server. No Python, no Jupyter, no conda. Open the file and explore in any modern browser.', S)}
|
||||
${this.uniqueCard('Multi-Domain', 'Transit detection, Dyson sphere search, habitability scoring, biosignature analysis — all in one causal event graph.', S)}
|
||||
${this.uniqueCard('Interactive 3D', 'Embedded Three.js dashboard: explore the causal atlas as a galaxy, rotate planet systems, visualize Dyson sphere geometry.', S)}
|
||||
</div>
|
||||
|
||||
<!-- ============ CAPABILITIES ============ -->
|
||||
<div style="${S.h2}" id="capabilities">Dashboard Views</div>
|
||||
<div style="${S.p}">12 interactive views, each pulling live data from the RVF file.</div>
|
||||
|
||||
${this.viewCard('cap-atlas', 'Atlas Explorer', '#/atlas', 'var(--accent)',
|
||||
'3D galaxy-style causal event graph. Each star = a causal event. Edges = cause-effect. Configurable arms, density, and sector labels.',
|
||||
['3D OrbitControls', 'Time scale selector', 'Galaxy shape config', 'Star map sectors'])}
|
||||
${this.viewCard('cap-coherence', 'Coherence Heatmap', '#/coherence', '#FFB020',
|
||||
'Color-mapped surface showing data self-consistency across the observation grid. Blue = stable, red = high uncertainty.',
|
||||
['Surface plot', 'Epoch scrubber', 'Partition boundaries'])}
|
||||
${this.viewCard('cap-boundaries', 'Boundaries', '#/boundaries', '#9944FF',
|
||||
'Tracks how data partition boundaries shift as new observations arrive. Alerts when boundaries change rapidly.',
|
||||
['Timeline chart', 'Alert feed', 'Sector detail'])}
|
||||
${this.viewCard('cap-memory', 'Memory Tiers', '#/memory', '#FF6B9D',
|
||||
'Three-tier storage: Small (hot), Medium (warm), Large (cold). Shows utilization, hit rates, and tier migration.',
|
||||
['S/M/L gauges', 'Utilization bars', 'Migration flow'])}
|
||||
${this.viewCard('cap-planets', 'Planet Candidates', '#/planets', '#2ECC71',
|
||||
'Ranked exoplanet candidates with radius, period, temperature, habitable zone status, and Earth Similarity Index.',
|
||||
['Sortable table', 'Light curve plots', 'Score radar'])}
|
||||
${this.viewCard('cap-life', 'Life Candidates', '#/life', '#2ECC71',
|
||||
'Biosignature analysis: atmospheric spectra for O\u2082, CH\u2084, H\u2082O. Multi-dimensional scoring with confound analysis.',
|
||||
['Spectrum plots', 'Molecule heatmap', 'Reaction graph'])}
|
||||
${this.viewCard('cap-witness', 'Witness Chain', '#/witness', 'var(--accent)',
|
||||
'Complete cryptographic audit trail. Every step with timestamp, hashes, and signature verification status.',
|
||||
['Scrolling entries', 'Hash verification', 'Pipeline trace'])}
|
||||
${this.viewCard('cap-solver', 'RVF Solver', '#/solver', '#FFB020',
|
||||
'WASM self-learning solver with Thompson Sampling. 3D landscape shows bandit arm rewards. Configurable training parameters.',
|
||||
['3D landscape', 'Training curves', 'A/B/C acceptance', 'Auto-Optimize'])}
|
||||
${this.viewCard('cap-blind', 'Blind Test', '#/blind-test', '#FF4D4D',
|
||||
'Pipeline on unlabeled data, then compared against ground truth. The gold standard for preventing overfitting.',
|
||||
['Unlabeled processing', 'Ground truth compare', 'Accuracy metrics'])}
|
||||
${this.viewCard('cap-discover', 'Discovery', '#/discover', '#00E5FF',
|
||||
'3D exoplanet systems with host star, orbit, habitable zone. Real KOI parameters. Galaxy background.',
|
||||
['3D planet system', 'Speed/rotate controls', 'ESI comparison'])}
|
||||
${this.viewCard('cap-dyson', 'Dyson Sphere', '#/dyson', '#9944FF',
|
||||
'Dyson swarm detection using Project Hephaistos methodology. IR excess analysis and 3D wireframe visualization.',
|
||||
['3D Dyson wireframe', 'IR excess analysis', 'SED plots'])}
|
||||
${this.viewCard('cap-status', 'System Status', '#/status', '#8B949E',
|
||||
'RVF file health, segment sizes, memory tier utilization, pipeline stage indicators, and live witness log.',
|
||||
['Segment breakdown', 'Tier gauges', 'Witness log feed'])}
|
||||
|
||||
<!-- ============ SOLVER ============ -->
|
||||
<div style="${S.h2}" id="solver">The Self-Learning Solver</div>
|
||||
<div style="${S.p}">
|
||||
The solver is a <span style="${S.accent}">WebAssembly module</span> compiled from Rust.
|
||||
It runs entirely in your browser using <strong>Thompson Sampling</strong>.
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="thompson">How Thompson Sampling Works</div>
|
||||
<div style="${S.p}">
|
||||
Imagine 8 different analysis strategies ("arms"). You don't know which works best.
|
||||
Thompson Sampling maintains a Beta distribution for each arm's success rate,
|
||||
samples from these on each attempt, and picks the highest sample. This balances:
|
||||
</div>
|
||||
<div style="${S.card}">
|
||||
<div style="font-size:12px;line-height:1.8">
|
||||
<span style="${S.accent}">Exploration</span> — Trying uncertain arms to gather data<br>
|
||||
<span style="${S.success}">Exploitation</span> — Using known-good arms to maximize results
|
||||
</div>
|
||||
<div style="font-size:10px;color:var(--text-muted);margin-top:6px">
|
||||
Over time, the solver converges on optimal strategies per difficulty level.
|
||||
The 3D landscape visually shows which arms have the highest rewards.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="auto-optimize">Auto-Optimize</div>
|
||||
<div style="${S.p}">
|
||||
The <span style="${S.success}">Auto-Optimize</span> button trains in batches of 3 rounds,
|
||||
tests acceptance after each batch, and stops when all three modes pass (max 30 rounds).
|
||||
If accuracy is below 60%, it automatically increases training intensity.
|
||||
</div>
|
||||
|
||||
<!-- ============ FORMAT ============ -->
|
||||
<div style="${S.h2}" id="format">RVF File Format Reference</div>
|
||||
|
||||
<div style="${S.h3}" id="file-header">File Header (64 bytes)</div>
|
||||
<pre style="${S.code}">Offset Size Field
|
||||
0x00 4 Magic: 0x52564631 ("RVF1")
|
||||
0x04 2 Format version (currently 1)
|
||||
0x06 2 Flags (bit 0 = signed, bit 1 = compressed)
|
||||
0x08 8 Total file size
|
||||
0x10 4 Segment count
|
||||
0x14 4 Reserved
|
||||
0x18 32 SHAKE-256 hash of all segments
|
||||
0x38 8 Creation timestamp (Unix epoch)</pre>
|
||||
|
||||
<div style="${S.h3}" id="seg-types">Segment Types</div>
|
||||
<div style="overflow-x:auto;margin-bottom:14px">
|
||||
<table style="width:100%;font-size:11px;font-family:var(--font-mono);border-collapse:collapse">
|
||||
<tr style="border-bottom:1px solid var(--border)">
|
||||
<th style="padding:6px 8px;text-align:left;color:var(--text-muted);font-weight:500;width:50px">ID</th>
|
||||
<th style="padding:6px 8px;text-align:left;color:var(--text-muted);font-weight:500;width:110px">Name</th>
|
||||
<th style="padding:6px 8px;text-align:left;color:var(--text-muted);font-weight:500">Purpose</th>
|
||||
</tr>
|
||||
${this.tableRow('0x01', 'DATA', 'Raw observations (light curves, spectra)')}
|
||||
${this.tableRow('0x02', 'KERNEL', 'Processing algorithms')}
|
||||
${this.tableRow('0x03', 'RESULT', 'Computed results and derived parameters')}
|
||||
${this.tableRow('0x04', 'WITNESS', 'Cryptographic audit trail')}
|
||||
${this.tableRow('0x05', 'SIGNATURE', 'Ed25519 digital signature')}
|
||||
${this.tableRow('0x06', 'INDEX', 'Fast lookup table for segments')}
|
||||
${this.tableRow('0x0F', 'EBPF', 'eBPF bytecode for in-kernel filtering')}
|
||||
${this.tableRow('0x10', 'WASM', 'WebAssembly solver module')}
|
||||
${this.tableRow('0x11', 'DASHBOARD', 'Embedded web dashboard (HTML/JS/CSS)')}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="${S.h3}" id="witness-format">Witness Entry Format</div>
|
||||
<pre style="${S.code}">struct WitnessEntry {
|
||||
step_name: String, // "transit_detection"
|
||||
timestamp: u64, // Unix epoch nanoseconds
|
||||
input_hash: [u8; 32], // SHAKE-256 of input
|
||||
output_hash: [u8; 32], // SHAKE-256 of output
|
||||
parent_hash: [u8; 32], // Previous entry hash (chain)
|
||||
signature: [u8; 64], // Ed25519 signature
|
||||
}</pre>
|
||||
|
||||
<div style="${S.h3}" id="dashboard-seg">Dashboard Segment</div>
|
||||
<pre style="${S.code}">DashboardHeader (64 bytes):
|
||||
magic: 0x5256_4442 // "RVDB"
|
||||
version: u16
|
||||
framework: u8 // 0=threejs, 1=react
|
||||
compression: u8 // 0=none, 1=gzip, 2=brotli
|
||||
bundle_size: u64
|
||||
file_count: u32
|
||||
hash: [u8; 32] // SHAKE-256 of bundle
|
||||
|
||||
Payload: [file_table] [file_data...]</pre>
|
||||
|
||||
<!-- ============ GLOSSARY ============ -->
|
||||
<div style="${S.h2}" id="glossary">Glossary</div>
|
||||
<div style="display:grid;grid-template-columns:130px 1fr;gap:1px 14px;font-size:12px;line-height:2">
|
||||
${this.glossaryRow('RVF', 'RuVector Format — the binary container')}
|
||||
${this.glossaryRow('Segment', 'A typed block of data inside an RVF file')}
|
||||
${this.glossaryRow('Witness Chain', 'Linked list of signed hash entries proving integrity')}
|
||||
${this.glossaryRow('SHAKE-256', 'Cryptographic hash function (variable output)')}
|
||||
${this.glossaryRow('Ed25519', 'Digital signature algorithm for witness entries')}
|
||||
${this.glossaryRow('KOI', 'Kepler Object of Interest — exoplanet candidate')}
|
||||
${this.glossaryRow('ESI', 'Earth Similarity Index (0-1, higher = more Earth-like)')}
|
||||
${this.glossaryRow('Transit', 'Planet passing in front of its star, causing a brightness dip')}
|
||||
${this.glossaryRow('Light Curve', 'Graph of stellar brightness over time')}
|
||||
${this.glossaryRow('Habitable Zone', 'Orbital region where liquid water could exist')}
|
||||
${this.glossaryRow('Thompson Samp.', 'Bandit algorithm balancing exploration vs exploitation')}
|
||||
${this.glossaryRow('eBPF', 'Extended Berkeley Packet Filter — fast kernel programs')}
|
||||
${this.glossaryRow('WASM', 'WebAssembly — portable code that runs in browsers')}
|
||||
${this.glossaryRow('Dyson Sphere', 'Hypothetical megastructure around a star for energy')}
|
||||
${this.glossaryRow('IR Excess', 'More infrared than expected — possible artificial origin')}
|
||||
${this.glossaryRow('SED', 'Spectral Energy Distribution — brightness vs wavelength')}
|
||||
${this.glossaryRow('Coherence', 'Self-consistency measure of data in a region')}
|
||||
${this.glossaryRow('Acceptance', 'Three-mode validation (A/B/C) of solver quality')}
|
||||
${this.glossaryRow('Blind Test', 'Evaluation on unlabeled data to prevent overfitting')}
|
||||
</div>
|
||||
|
||||
<div style="margin-top:48px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--text-muted);text-align:center">
|
||||
Everything in this dashboard was served from a single <code style="${S.inline}">.rvf</code> file.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ── Template helpers ── */
|
||||
|
||||
private statCard(label: string, value: string, S: Record<string, string>): string {
|
||||
return `<div style="${S.card}padding:10px 12px;text-align:center">
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:2px">${label}</div>
|
||||
<div style="font-size:12px;font-weight:600;color:var(--accent);font-family:var(--font-mono)">${value}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private segRow(name: string, desc: string, color: string): string {
|
||||
return `<div style="display:flex;align-items:center;gap:10px"><span style="color:${color};min-width:160px;font-weight:600">${name}</span><span style="color:var(--text-secondary);font-weight:400">${desc}</span></div>`;
|
||||
}
|
||||
|
||||
private uniqueCard(title: string, desc: string, S: Record<string, string>): string {
|
||||
return `<div style="${S.card}"><div style="font-size:12px;${S.accent}margin-bottom:4px">${title}</div><div style="font-size:11px;line-height:1.5">${desc}</div></div>`;
|
||||
}
|
||||
|
||||
private viewCard(id: string, title: string, route: string, color: string, desc: string, features: string[]): string {
|
||||
const badges = features.map(f => `<span style="font-size:9px;padding:2px 6px;border-radius:3px;background:rgba(0,229,255,0.06);border:1px solid rgba(0,229,255,0.1);color:var(--accent)">${f}</span>`).join('');
|
||||
return `<div id="${id}" style="background:var(--bg-panel);border:1px solid var(--border);border-radius:6px;padding:12px 16px;margin-bottom:8px;border-left:3px solid ${color}">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||||
<span style="font-size:12px;font-weight:600;color:var(--text-primary)">${title}</span>
|
||||
<a href="${route}" style="font-size:10px;color:${color};font-family:var(--font-mono);text-decoration:none">${route}</a>
|
||||
</div>
|
||||
<div style="font-size:11px;line-height:1.5;margin-bottom:6px">${desc}</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:3px">${badges}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private tableRow(id: string, name: string, purpose: string): string {
|
||||
return `<tr style="border-bottom:1px solid var(--border-subtle)"><td style="padding:5px 8px;color:var(--accent)">${id}</td><td style="padding:5px 8px;color:var(--text-primary)">${name}</td><td style="padding:5px 8px">${purpose}</td></tr>`;
|
||||
}
|
||||
|
||||
private glossaryRow(term: string, def: string): string {
|
||||
return `<span style="color:var(--accent);font-weight:600">${term}</span><span>${def}</span>`;
|
||||
}
|
||||
}
|
||||
229
vendor/ruvector/examples/rvf/dashboard/src/views/DownloadView.ts
vendored
Normal file
229
vendor/ruvector/examples/rvf/dashboard/src/views/DownloadView.ts
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* DownloadView — Download page for RVF executables and packages.
|
||||
*
|
||||
* Provides download links for:
|
||||
* - Windows (.exe)
|
||||
* - macOS (.dmg)
|
||||
* - Linux (.tar.gz)
|
||||
* - npm packages
|
||||
* - WASM module
|
||||
*
|
||||
* Download URLs point to Google Cloud Storage (placeholder paths).
|
||||
*/
|
||||
|
||||
const VERSION = '2.0.0';
|
||||
const BASE_URL = 'https://storage.googleapis.com/ruvector-releases';
|
||||
|
||||
interface DownloadItem {
|
||||
platform: string;
|
||||
icon: string;
|
||||
file: string;
|
||||
size: string;
|
||||
ext: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
const DOWNLOADS: DownloadItem[] = [
|
||||
{
|
||||
platform: 'Windows',
|
||||
icon: '<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>',
|
||||
file: `ruvector-${VERSION}-x64.exe`,
|
||||
size: '~12 MB',
|
||||
ext: '.exe',
|
||||
desc: 'Windows 10/11 (x64) installer with bundled WASM runtime',
|
||||
},
|
||||
{
|
||||
platform: 'macOS',
|
||||
icon: '<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/><path d="M15 8.5c0-1-0.67-2.5-2-2.5S11 7.5 11 8.5c0 1.5 1 2 1 3.5s-1 2-1 3.5c0 1 0.67 2.5 2 2.5s2-1.5 2-2.5c0-1.5-1-2-1-3.5s1-2 1-3.5z"/></svg>',
|
||||
file: `RuVector-${VERSION}.dmg`,
|
||||
size: '~14 MB',
|
||||
ext: '.dmg',
|
||||
desc: 'macOS 12+ (Apple Silicon & Intel) disk image',
|
||||
},
|
||||
{
|
||||
platform: 'Linux',
|
||||
icon: '<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/><circle cx="12" cy="8" r="1.5"/></svg>',
|
||||
file: `ruvector-${VERSION}-linux-x64.tar.gz`,
|
||||
size: '~10 MB',
|
||||
ext: '.tar.gz',
|
||||
desc: 'Linux (x86_64) tarball — Ubuntu 20+, Debian 11+, Fedora 36+',
|
||||
},
|
||||
];
|
||||
|
||||
export class DownloadView {
|
||||
private container: HTMLElement | null = null;
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'max-width:960px;margin:0 auto;padding:32px 24px;overflow-y:auto;height:100%';
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// Hero
|
||||
const hero = document.createElement('div');
|
||||
hero.style.cssText = 'text-align:center;margin-bottom:40px';
|
||||
hero.innerHTML = `
|
||||
<div style="display:inline-flex;align-items:center;gap:12px;margin-bottom:16px">
|
||||
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="#00E5FF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="6"/><ellipse cx="12" cy="12" rx="11" ry="4" transform="rotate(-20 12 12)"/>
|
||||
<circle cx="12" cy="12" r="1.5" fill="#00E5FF" stroke="none"/>
|
||||
</svg>
|
||||
<span style="font-size:28px;font-weight:300;color:var(--text-primary);letter-spacing:2px">RuVector</span>
|
||||
</div>
|
||||
<div style="font-size:14px;color:var(--text-secondary);line-height:1.6;max-width:600px;margin:0 auto">
|
||||
Download the Causal Atlas runtime — a single binary that reads <code style="color:var(--accent);font-size:12px">.rvf</code> files,
|
||||
runs the WASM solver, serves the Three.js dashboard, and verifies the Ed25519 witness chain.
|
||||
</div>
|
||||
<div style="margin-top:12px;display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
|
||||
<span style="font-size:10px;padding:3px 8px;border-radius:4px;background:rgba(0,229,255,0.08);border:1px solid rgba(0,229,255,0.15);color:#00E5FF">v${VERSION}</span>
|
||||
<span style="font-size:10px;padding:3px 8px;border-radius:4px;background:rgba(46,204,113,0.08);border:1px solid rgba(46,204,113,0.15);color:#2ECC71">Stable</span>
|
||||
<span style="font-size:10px;padding:3px 8px;border-radius:4px;background:rgba(255,176,32,0.08);border:1px solid rgba(255,176,32,0.15);color:#FFB020">ADR-040</span>
|
||||
</div>
|
||||
`;
|
||||
wrapper.appendChild(hero);
|
||||
|
||||
// Download cards
|
||||
const grid = document.createElement('div');
|
||||
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:16px;margin-bottom:40px';
|
||||
wrapper.appendChild(grid);
|
||||
|
||||
for (const dl of DOWNLOADS) {
|
||||
const card = document.createElement('div');
|
||||
card.style.cssText = `
|
||||
background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;
|
||||
padding:20px;display:flex;flex-direction:column;gap:12px;
|
||||
transition:border-color 0.2s,transform 0.2s;cursor:pointer;
|
||||
`;
|
||||
card.addEventListener('mouseenter', () => {
|
||||
card.style.borderColor = 'rgba(0,229,255,0.3)';
|
||||
card.style.transform = 'translateY(-2px)';
|
||||
});
|
||||
card.addEventListener('mouseleave', () => {
|
||||
card.style.borderColor = 'var(--border)';
|
||||
card.style.transform = '';
|
||||
});
|
||||
|
||||
card.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<div style="color:#00E5FF">${dl.icon}</div>
|
||||
<div>
|
||||
<div style="font-size:15px;font-weight:600;color:var(--text-primary)">${dl.platform}</div>
|
||||
<div style="font-size:10px;color:var(--text-muted)">${dl.size}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.5">${dl.desc}</div>
|
||||
<div style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted);padding:6px 8px;background:rgba(0,0,0,0.3);border-radius:4px;word-break:break-all">${dl.file}</div>
|
||||
<a href="${BASE_URL}/v${VERSION}/${dl.file}" style="
|
||||
display:flex;align-items:center;justify-content:center;gap:6px;
|
||||
padding:8px 16px;border-radius:6px;text-decoration:none;
|
||||
background:rgba(0,229,255,0.1);border:1px solid rgba(0,229,255,0.25);
|
||||
color:#00E5FF;font-size:12px;font-weight:600;transition:background 0.15s;
|
||||
" onmouseenter="this.style.background='rgba(0,229,255,0.2)'" onmouseleave="this.style.background='rgba(0,229,255,0.1)'">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Download ${dl.ext}
|
||||
</a>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
}
|
||||
|
||||
// npm / WASM section
|
||||
const altSection = document.createElement('div');
|
||||
altSection.style.cssText = 'margin-bottom:40px';
|
||||
altSection.innerHTML = `
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:16px;display:flex;align-items:center;gap:8px">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
||||
npm Packages & WASM Module
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;padding:14px">
|
||||
<div style="font-size:11px;font-weight:600;color:var(--text-primary);margin-bottom:6px">rvf-solver (npm)</div>
|
||||
<code style="display:block;font-size:10px;color:var(--accent);background:rgba(0,0,0,0.3);padding:8px;border-radius:4px;margin-bottom:6px">npm install @ruvector/rvf-solver</code>
|
||||
<div style="font-size:10px;color:var(--text-muted);line-height:1.4">NAPI-RS native bindings for Node.js — includes solver, witness chain, and policy kernel.</div>
|
||||
</div>
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;padding:14px">
|
||||
<div style="font-size:11px;font-weight:600;color:var(--text-primary);margin-bottom:6px">rvf-solver-wasm (npm)</div>
|
||||
<code style="display:block;font-size:10px;color:var(--accent);background:rgba(0,0,0,0.3);padding:8px;border-radius:4px;margin-bottom:6px">npm install @ruvector/rvf-solver-wasm</code>
|
||||
<div style="font-size:10px;color:var(--text-muted);line-height:1.4">Browser WASM module — same solver running in this dashboard. No native dependencies.</div>
|
||||
</div>
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;padding:14px">
|
||||
<div style="font-size:11px;font-weight:600;color:var(--text-primary);margin-bottom:6px">Standalone WASM</div>
|
||||
<code style="display:block;font-size:10px;color:var(--accent);background:rgba(0,0,0,0.3);padding:8px;border-radius:4px;margin-bottom:6px">curl -O ${BASE_URL}/v${VERSION}/rvf_solver_wasm.wasm</code>
|
||||
<div style="font-size:10px;color:var(--text-muted);line-height:1.4">Raw <code>.wasm</code> binary (172 KB). Load via WebAssembly.instantiate() — no wasm-bindgen needed.</div>
|
||||
</div>
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;padding:14px">
|
||||
<div style="font-size:11px;font-weight:600;color:var(--text-primary);margin-bottom:6px">Cargo Crate</div>
|
||||
<code style="display:block;font-size:10px;color:var(--accent);background:rgba(0,0,0,0.3);padding:8px;border-radius:4px;margin-bottom:6px">cargo add rvf-runtime rvf-types rvf-crypto</code>
|
||||
<div style="font-size:10px;color:var(--text-muted);line-height:1.4">Rust workspace crates for embedding RVF files in your own applications.</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
wrapper.appendChild(altSection);
|
||||
|
||||
// Quick Start section
|
||||
const quickstart = document.createElement('div');
|
||||
quickstart.style.cssText = 'margin-bottom:40px';
|
||||
quickstart.innerHTML = `
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:16px;display:flex;align-items:center;gap:8px">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||
Quick Start
|
||||
</div>
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:20px">
|
||||
<div style="display:grid;gap:16px">
|
||||
${this.step(1, 'Download', 'Download the installer for your platform above and run it.')}
|
||||
${this.step(2, 'Open an RVF file', `
|
||||
<code style="display:block;font-size:11px;color:var(--accent);background:rgba(0,0,0,0.3);padding:8px;border-radius:4px;margin-top:4px">ruvector open causal_atlas.rvf</code>
|
||||
<div style="font-size:10px;color:var(--text-muted);margin-top:4px">This starts the local server and opens the dashboard in your browser.</div>
|
||||
`)}
|
||||
${this.step(3, 'Train the solver', 'Navigate to the Solver page and click Train or Auto-Optimize. The WASM solver learns in real time inside your browser.')}
|
||||
${this.step(4, 'Run acceptance test', 'Click Acceptance to verify the solver passes the three-mode acceptance test (A/B/C). All results are recorded in the Ed25519 witness chain.')}
|
||||
${this.step(5, 'Explore discoveries', `Navigate to Planets, Life, and Discover pages to explore candidate detections. Each candidate includes a full causal trace and witness proof.`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
wrapper.appendChild(quickstart);
|
||||
|
||||
// System requirements
|
||||
const reqs = document.createElement('div');
|
||||
reqs.innerHTML = `
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:16px;display:flex;align-items:center;gap:8px">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="4" y1="9" x2="20" y2="9"/><circle cx="8" cy="6.5" r="0.5" fill="currentColor" stroke="none"/></svg>
|
||||
System Requirements
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;font-size:11px">
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;padding:12px">
|
||||
<div style="font-weight:600;color:var(--text-primary);margin-bottom:6px">Windows</div>
|
||||
<div style="color:var(--text-muted);line-height:1.5">Windows 10 (1903+)<br>x64 processor<br>4 GB RAM<br>100 MB disk</div>
|
||||
</div>
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;padding:12px">
|
||||
<div style="font-weight:600;color:var(--text-primary);margin-bottom:6px">macOS</div>
|
||||
<div style="color:var(--text-muted);line-height:1.5">macOS 12 Monterey+<br>Apple Silicon or Intel<br>4 GB RAM<br>100 MB disk</div>
|
||||
</div>
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;padding:12px">
|
||||
<div style="font-weight:600;color:var(--text-primary);margin-bottom:6px">Linux</div>
|
||||
<div style="color:var(--text-muted);line-height:1.5">glibc 2.31+<br>x86_64 processor<br>4 GB RAM<br>100 MB disk</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:9px;color:var(--text-muted);margin-top:12px;text-align:center">
|
||||
Binaries are hosted on Google Cloud Storage. All downloads include Ed25519 signatures for verification.
|
||||
</div>
|
||||
`;
|
||||
wrapper.appendChild(reqs);
|
||||
}
|
||||
|
||||
private step(n: number, title: string, detail: string): string {
|
||||
return `
|
||||
<div style="display:flex;gap:12px;align-items:flex-start">
|
||||
<div style="min-width:24px;height:24px;border-radius:50%;background:rgba(0,229,255,0.1);border:1px solid rgba(0,229,255,0.25);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#00E5FF">${n}</div>
|
||||
<div style="flex:1">
|
||||
<div style="font-size:12px;font-weight:600;color:var(--text-primary);margin-bottom:2px">${title}</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.5">${detail}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
this.container = null;
|
||||
}
|
||||
}
|
||||
1176
vendor/ruvector/examples/rvf/dashboard/src/views/DysonSphereView.ts
vendored
Normal file
1176
vendor/ruvector/examples/rvf/dashboard/src/views/DysonSphereView.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
422
vendor/ruvector/examples/rvf/dashboard/src/views/LifeDashboard.ts
vendored
Normal file
422
vendor/ruvector/examples/rvf/dashboard/src/views/LifeDashboard.ts
vendored
Normal file
@@ -0,0 +1,422 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { SpectrumChart, SpectrumPoint, SpectrumBand } from '../charts/SpectrumChart';
|
||||
import { fetchLifeCandidates, LifeCandidate } from '../api';
|
||||
|
||||
const BIOSIG_BANDS: SpectrumBand[] = [
|
||||
{ name: 'O2', start: 0.76, end: 0.78, color: '#58A6FF' },
|
||||
{ name: 'H2O', start: 0.93, end: 0.97, color: '#00E5FF' },
|
||||
{ name: 'CH4', start: 1.65, end: 1.70, color: '#2ECC71' },
|
||||
{ name: 'CO2', start: 2.00, end: 2.08, color: '#FFB020' },
|
||||
{ name: 'O3', start: 0.55, end: 0.60, color: '#9944ff' },
|
||||
];
|
||||
|
||||
interface MoleculeNode {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
interface MoleculeEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
/** Generate demo spectrum data for a life candidate. */
|
||||
function demoSpectrum(candidate: LifeCandidate): SpectrumPoint[] {
|
||||
const points: SpectrumPoint[] = [];
|
||||
for (let w = 0.4; w <= 2.5; w += 0.005) {
|
||||
let flux = 0.8 + 0.1 * Math.sin(w * 3);
|
||||
// Add absorption features
|
||||
if (candidate.o2 > 0.3 && w > 0.76 && w < 0.78) flux -= candidate.o2 * 0.3;
|
||||
if (candidate.h2o > 0.3 && w > 0.93 && w < 0.97) flux -= candidate.h2o * 0.25;
|
||||
if (candidate.ch4 > 0.3 && w > 1.65 && w < 1.70) flux -= candidate.ch4 * 0.2;
|
||||
flux += (Math.random() - 0.5) * 0.02;
|
||||
points.push({ wavelength: w, flux: Math.max(0, flux) });
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
/** Build a simple molecule reaction graph. */
|
||||
function buildMoleculeGraph(): { nodes: MoleculeNode[]; edges: MoleculeEdge[] } {
|
||||
const molecules = ['O2', 'H2O', 'CH4', 'CO2', 'O3', 'N2O', 'NH3'];
|
||||
const nodes: MoleculeNode[] = molecules.map((id, i) => {
|
||||
const angle = (i / molecules.length) * Math.PI * 2;
|
||||
return { id, x: Math.cos(angle) * 2, y: Math.sin(angle) * 2, z: (Math.random() - 0.5) * 0.5 };
|
||||
});
|
||||
const edges: MoleculeEdge[] = [
|
||||
{ source: 'O2', target: 'O3' },
|
||||
{ source: 'H2O', target: 'O2' },
|
||||
{ source: 'CH4', target: 'CO2' },
|
||||
{ source: 'CH4', target: 'H2O' },
|
||||
{ source: 'N2O', target: 'O2' },
|
||||
{ source: 'NH3', target: 'N2O' },
|
||||
{ source: 'CO2', target: 'O2' },
|
||||
];
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
export class LifeDashboard {
|
||||
private container: HTMLElement | null = null;
|
||||
private candidates: LifeCandidate[] = [];
|
||||
private selectedId: string | null = null;
|
||||
private spectrumChart: SpectrumChart | null = null;
|
||||
private tableBody: HTMLTableSectionElement | null = null;
|
||||
private confoundBar: HTMLElement | null = null;
|
||||
|
||||
// Three.js for molecule graph
|
||||
private renderer: THREE.WebGLRenderer | null = null;
|
||||
private scene: THREE.Scene | null = null;
|
||||
private camera: THREE.PerspectiveCamera | null = null;
|
||||
private controls: OrbitControls | null = null;
|
||||
private animFrameId = 0;
|
||||
private moleculeMeshes: THREE.Object3D[] = [];
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden';
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// View header with explanation
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'padding:12px 20px;border-bottom:1px solid var(--border);flex-shrink:0';
|
||||
header.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||||
<div style="font-size:14px;font-weight:600;color:var(--text-primary)">Biosignature Analysis — Real Atmospheric Data</div>
|
||||
<span class="score-badge score-high" style="font-size:9px;padding:1px 6px">JWST</span>
|
||||
<span class="score-badge score-medium" style="font-size:9px;padding:1px 6px">8 TARGETS</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.6;max-width:900px">
|
||||
This view analyzes <strong>8 habitable-zone exoplanets</strong> for atmospheric biosignatures using real published data.
|
||||
<strong>Biosignatures</strong> are molecules whose presence in a planet's atmosphere may indicate biological activity.
|
||||
Click any row to inspect its spectrum and confound analysis.
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:8px;font-size:10px">
|
||||
<div style="background:rgba(0,229,255,0.06);border:1px solid rgba(0,229,255,0.15);border-radius:4px;padding:6px 8px">
|
||||
<div style="color:var(--accent);font-weight:600;margin-bottom:2px">What is JWST?</div>
|
||||
<div style="color:var(--text-secondary);line-height:1.4">The James Webb Space Telescope observes exoplanet atmospheres via <strong>transmission spectroscopy</strong> — starlight passing through a planet's atmosphere reveals molecular absorption lines. Only <span style="color:var(--success);font-weight:600">K2-18 b</span> has confirmed detections so far (CH<sub>4</sub>+CO<sub>2</sub>).</div>
|
||||
</div>
|
||||
<div style="background:rgba(46,204,113,0.06);border:1px solid rgba(46,204,113,0.15);border-radius:4px;padding:6px 8px">
|
||||
<div style="color:var(--success);font-weight:600;margin-bottom:2px">Key Molecules</div>
|
||||
<div style="color:var(--text-secondary);line-height:1.4"><strong>O<sub>2</sub></strong> (oxygen) — product of photosynthesis. <strong>CH<sub>4</sub></strong> (methane) — produced by methanogens. <strong>H<sub>2</sub>O</strong> (water) — essential solvent. <strong>CO<sub>2</sub></strong> — greenhouse gas. <strong>DMS</strong> — dimethyl sulfide, only known biogenic source on Earth.</div>
|
||||
</div>
|
||||
<div style="background:rgba(255,176,32,0.06);border:1px solid rgba(255,176,32,0.15);border-radius:4px;padding:6px 8px">
|
||||
<div style="color:var(--warning);font-weight:600;margin-bottom:2px">Disequilibrium & Confounds</div>
|
||||
<div style="color:var(--text-secondary);line-height:1.4"><strong>Thermodynamic disequilibrium</strong>: CH<sub>4</sub>+CO<sub>2</sub> coexisting implies an active source replenishing CH<sub>4</sub> — possibly biological. <strong>Confound index</strong> = probability that detected signals have a non-biological explanation (volcanism, photochemistry, etc.).</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
wrapper.appendChild(header);
|
||||
|
||||
const layout = document.createElement('div');
|
||||
layout.className = 'split-layout';
|
||||
layout.style.flex = '1';
|
||||
layout.style.minHeight = '0';
|
||||
wrapper.appendChild(layout);
|
||||
|
||||
// Left panel: table + confound bars
|
||||
const left = document.createElement('div');
|
||||
left.className = 'left-panel';
|
||||
layout.appendChild(left);
|
||||
|
||||
const tableArea = document.createElement('div');
|
||||
tableArea.className = 'table-area';
|
||||
left.appendChild(tableArea);
|
||||
|
||||
const table = document.createElement('table');
|
||||
table.className = 'data-table';
|
||||
const thead = document.createElement('thead');
|
||||
const headerRow = document.createElement('tr');
|
||||
for (const label of ['Name', 'Score', 'JWST', 'O2', 'CH4', 'H2O', 'Diseq.']) {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = label;
|
||||
headerRow.appendChild(th);
|
||||
}
|
||||
thead.appendChild(headerRow);
|
||||
table.appendChild(thead);
|
||||
this.tableBody = document.createElement('tbody');
|
||||
table.appendChild(this.tableBody);
|
||||
tableArea.appendChild(table);
|
||||
|
||||
// Confound indicator
|
||||
const confoundArea = document.createElement('div');
|
||||
confoundArea.className = 'chart-area';
|
||||
confoundArea.style.padding = '12px 16px';
|
||||
left.appendChild(confoundArea);
|
||||
|
||||
const confLabel = document.createElement('div');
|
||||
confLabel.className = 'panel-header';
|
||||
confLabel.innerHTML = 'Confound Index <span style="font-size:8px;text-transform:none;letter-spacing:0;color:var(--text-muted);font-weight:400">probability of non-biological origin</span>';
|
||||
confoundArea.appendChild(confLabel);
|
||||
|
||||
this.confoundBar = document.createElement('div');
|
||||
this.confoundBar.style.marginTop = '12px';
|
||||
confoundArea.appendChild(this.confoundBar);
|
||||
|
||||
// Right panel: spectrum + molecule graph
|
||||
const right = document.createElement('div');
|
||||
right.className = 'right-panel';
|
||||
layout.appendChild(right);
|
||||
|
||||
const specDiv = document.createElement('div');
|
||||
specDiv.style.height = '220px';
|
||||
specDiv.style.minHeight = '200px';
|
||||
right.appendChild(specDiv);
|
||||
this.spectrumChart = new SpectrumChart(specDiv);
|
||||
|
||||
const molDiv = document.createElement('div');
|
||||
molDiv.className = 'three-container';
|
||||
molDiv.style.flex = '1';
|
||||
molDiv.style.minHeight = '200px';
|
||||
right.appendChild(molDiv);
|
||||
|
||||
// Three.js molecule graph
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x0B0F14);
|
||||
this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
|
||||
this.camera.position.set(0, 0, 6);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
molDiv.appendChild(this.renderer.domElement);
|
||||
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
dl.position.set(3, 5, 3);
|
||||
this.scene.add(dl);
|
||||
|
||||
this.buildMoleculeScene();
|
||||
|
||||
window.addEventListener('resize', this.resize);
|
||||
this.resize();
|
||||
this.animate();
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
private buildMoleculeScene(): void {
|
||||
if (!this.scene) return;
|
||||
|
||||
const { nodes, edges } = buildMoleculeGraph();
|
||||
const nodeMap = new Map<string, MoleculeNode>();
|
||||
|
||||
const colors: Record<string, number> = {
|
||||
O2: 0x58A6FF, H2O: 0x00E5FF, CH4: 0x2ECC71,
|
||||
CO2: 0xFFB020, O3: 0x9944ff, N2O: 0xFFB020, NH3: 0xFF4D4D,
|
||||
};
|
||||
|
||||
for (const node of nodes) {
|
||||
nodeMap.set(node.id, node);
|
||||
const geo = new THREE.SphereGeometry(0.2, 16, 12);
|
||||
const mat = new THREE.MeshStandardMaterial({ color: colors[node.id] ?? 0x888888 });
|
||||
const mesh = new THREE.Mesh(geo, mat);
|
||||
mesh.position.set(node.x, node.y, node.z);
|
||||
this.scene.add(mesh);
|
||||
this.moleculeMeshes.push(mesh);
|
||||
|
||||
// Label sprite
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128;
|
||||
canvas.height = 48;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.fillStyle = '#E6EDF3';
|
||||
ctx.font = '24px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(node.id, 64, 32);
|
||||
}
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true });
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
sprite.position.set(node.x, node.y + 0.35, node.z);
|
||||
sprite.scale.set(0.8, 0.3, 1);
|
||||
this.scene.add(sprite);
|
||||
this.moleculeMeshes.push(sprite);
|
||||
}
|
||||
|
||||
// Edges
|
||||
const positions: number[] = [];
|
||||
for (const edge of edges) {
|
||||
const src = nodeMap.get(edge.source);
|
||||
const tgt = nodeMap.get(edge.target);
|
||||
if (!src || !tgt) continue;
|
||||
positions.push(src.x, src.y, src.z, tgt.x, tgt.y, tgt.z);
|
||||
}
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
const mat = new THREE.LineBasicMaterial({ color: 0x1C2333, transparent: true, opacity: 0.6 });
|
||||
const lines = new THREE.LineSegments(geo, mat);
|
||||
this.scene.add(lines);
|
||||
this.moleculeMeshes.push(lines);
|
||||
}
|
||||
|
||||
private async loadData(): Promise<void> {
|
||||
try {
|
||||
this.candidates = await fetchLifeCandidates();
|
||||
} catch (err) {
|
||||
console.error('Life API error:', err);
|
||||
this.candidates = [];
|
||||
}
|
||||
this.renderTable();
|
||||
if (this.candidates.length > 0) {
|
||||
this.selectCandidate(this.candidates[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
private renderTable(): void {
|
||||
if (!this.tableBody) return;
|
||||
this.tableBody.innerHTML = '';
|
||||
|
||||
const sorted = [...this.candidates].sort((a, b) => b.score - a.score);
|
||||
for (const c of sorted) {
|
||||
const tr = document.createElement('tr');
|
||||
if (c.id === this.selectedId) tr.classList.add('selected');
|
||||
tr.addEventListener('click', () => this.selectCandidate(c.id));
|
||||
|
||||
// Name
|
||||
const tdName = document.createElement('td');
|
||||
tdName.textContent = c.name;
|
||||
tr.appendChild(tdName);
|
||||
|
||||
// Score
|
||||
const tdScore = document.createElement('td');
|
||||
tdScore.textContent = c.score.toFixed(2);
|
||||
tr.appendChild(tdScore);
|
||||
|
||||
// JWST status
|
||||
const tdJwst = document.createElement('td');
|
||||
if (c.jwstObserved) {
|
||||
if (c.moleculesConfirmed.length > 0) {
|
||||
tdJwst.innerHTML = `<span class="score-badge score-high" style="font-size:8px">${c.moleculesConfirmed.join('+')}</span>`;
|
||||
} else {
|
||||
tdJwst.innerHTML = '<span class="score-badge score-medium" style="font-size:8px">OBS</span>';
|
||||
}
|
||||
} else {
|
||||
tdJwst.innerHTML = '<span style="color:var(--text-muted);font-size:9px">--</span>';
|
||||
}
|
||||
tr.appendChild(tdJwst);
|
||||
|
||||
// O2, CH4, H2O, Diseq
|
||||
for (const v of [c.o2.toFixed(2), c.ch4.toFixed(2), c.h2o.toFixed(2), c.disequilibrium.toFixed(2)]) {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = v;
|
||||
tr.appendChild(td);
|
||||
}
|
||||
this.tableBody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
private selectCandidate(id: string): void {
|
||||
this.selectedId = id;
|
||||
this.renderTable();
|
||||
|
||||
const c = this.candidates.find((l) => l.id === id);
|
||||
if (!c) return;
|
||||
|
||||
// Spectrum
|
||||
const specData = demoSpectrum(c);
|
||||
this.spectrumChart?.update(specData, BIOSIG_BANDS);
|
||||
|
||||
// Confound bar + atmosphere status + detailed breakdown
|
||||
if (this.confoundBar) {
|
||||
const confound = 1 - c.disequilibrium;
|
||||
const confoundLabel = confound > 0.7 ? 'Likely abiotic' : confound > 0.4 ? 'Ambiguous' : 'Possibly biogenic';
|
||||
const confoundExplain = confound > 0.7
|
||||
? 'Most detected signals can be explained by geological or photochemical processes without invoking biology.'
|
||||
: confound > 0.4
|
||||
? 'Some signals are consistent with both biological and abiotic origins. Further data needed to distinguish.'
|
||||
: 'Detected molecular combination is difficult to explain without an active biological source. Strongest biosignature candidates.';
|
||||
|
||||
this.confoundBar.innerHTML = `
|
||||
<div class="progress-label">
|
||||
<span>Confound likelihood</span>
|
||||
<span style="color:${confound > 0.6 ? 'var(--danger, #FF4D4D)' : confound > 0.3 ? 'var(--warning)' : 'var(--success)'};font-weight:600">${(confound * 100).toFixed(0)}% — ${confoundLabel}</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill ${confound > 0.6 ? 'danger' : confound > 0.3 ? 'warning' : 'success'}" style="width: ${confound * 100}%"></div>
|
||||
</div>
|
||||
<div style="font-size:9px;color:var(--text-muted);margin-top:4px;line-height:1.4">${confoundExplain}</div>
|
||||
<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:4px;padding:6px 8px">
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.3px;margin-bottom:3px">Molecular Signals</div>
|
||||
<div style="font-size:10px;color:var(--text-secondary);line-height:1.5">
|
||||
<div>O<sub>2</sub>: <span style="color:${c.o2 > 0.5 ? 'var(--success)' : 'var(--text-muted)'}">${c.o2 > 0.01 ? (c.o2 * 100).toFixed(0) + '%' : 'Not detected'}</span></div>
|
||||
<div>CH<sub>4</sub>: <span style="color:${c.ch4 > 0.5 ? 'var(--success)' : 'var(--text-muted)'}">${c.ch4 > 0.01 ? (c.ch4 * 100).toFixed(0) + '%' : 'Not detected'}</span></div>
|
||||
<div>H<sub>2</sub>O: <span style="color:${c.h2o > 0.5 ? '#00E5FF' : 'var(--text-muted)'}">${c.h2o > 0.01 ? (c.h2o * 100).toFixed(0) + '%' : 'Not detected'}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:4px;padding:6px 8px">
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.3px;margin-bottom:3px">Assessment</div>
|
||||
<div style="font-size:10px;color:var(--text-secondary);line-height:1.5">
|
||||
<div>Diseq.: <span style="color:${c.disequilibrium > 0.5 ? 'var(--success)' : 'var(--text-muted)'}">${(c.disequilibrium * 100).toFixed(0)}%</span></div>
|
||||
<div>Habitability: <span style="color:var(--accent)">${(c.habitability * 100).toFixed(0)}%</span></div>
|
||||
<div>JWST: ${c.jwstObserved ? (c.moleculesConfirmed.length > 0 ? '<span style="color:var(--success)">' + c.moleculesConfirmed.join(', ') + '</span>' : '<span style="color:var(--warning)">Observed, no detections</span>') : '<span style="color:var(--text-muted)">Not yet observed</span>'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:10px;font-size:10px;color:var(--text-secondary);line-height:1.5;border-top:1px solid var(--border);padding-top:8px">
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:3px">Atmosphere Status</div>
|
||||
${c.atmosphereStatus}
|
||||
</div>
|
||||
${c.reference ? `<div style="margin-top:6px;font-size:9px;color:var(--text-muted);font-style:italic">${c.reference}</div>` : ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
private resize = (): void => {
|
||||
if (!this.renderer || !this.camera) return;
|
||||
const canvasEl = this.renderer.domElement.parentElement;
|
||||
if (!canvasEl) return;
|
||||
const w = canvasEl.clientWidth;
|
||||
const h = canvasEl.clientHeight;
|
||||
this.renderer.setSize(w, h);
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
private animate = (): void => {
|
||||
this.animFrameId = requestAnimationFrame(this.animate);
|
||||
this.controls?.update();
|
||||
if (this.renderer && this.scene && this.camera) {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
};
|
||||
|
||||
unmount(): void {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
cancelAnimationFrame(this.animFrameId);
|
||||
|
||||
this.spectrumChart?.destroy();
|
||||
|
||||
// Dispose molecule meshes
|
||||
for (const obj of this.moleculeMeshes) {
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.geometry.dispose();
|
||||
(obj.material as THREE.Material).dispose();
|
||||
} else if (obj instanceof THREE.LineSegments) {
|
||||
obj.geometry.dispose();
|
||||
(obj.material as THREE.Material).dispose();
|
||||
} else if (obj instanceof THREE.Sprite) {
|
||||
obj.material.map?.dispose();
|
||||
obj.material.dispose();
|
||||
}
|
||||
this.scene?.remove(obj);
|
||||
}
|
||||
this.moleculeMeshes = [];
|
||||
|
||||
this.controls?.dispose();
|
||||
this.renderer?.dispose();
|
||||
|
||||
this.spectrumChart = null;
|
||||
this.controls = null;
|
||||
this.renderer = null;
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.container = null;
|
||||
}
|
||||
}
|
||||
240
vendor/ruvector/examples/rvf/dashboard/src/views/MemoryView.ts
vendored
Normal file
240
vendor/ruvector/examples/rvf/dashboard/src/views/MemoryView.ts
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
import { fetchMemoryTiers, MemoryTiers } from '../api';
|
||||
|
||||
export class MemoryView {
|
||||
private container: HTMLElement | null = null;
|
||||
private gaugesEl: HTMLElement | null = null;
|
||||
private detailEl: HTMLElement | null = null;
|
||||
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'grid-12';
|
||||
container.appendChild(grid);
|
||||
|
||||
// View header with explanation
|
||||
const header = document.createElement('div');
|
||||
header.className = 'col-12';
|
||||
header.style.cssText = 'padding:4px 0 8px 0';
|
||||
header.innerHTML = `
|
||||
<div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:2px">Memory Tiers</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.5">
|
||||
RVF uses a <span style="color:var(--accent)">3-tier memory hierarchy</span> for vector storage and retrieval.
|
||||
<strong>S (Hot/L1)</strong> = fastest access (<1μs), recent data in CPU cache.
|
||||
<strong>M (Warm/HNSW)</strong> = indexed vectors (~12μs), approximate nearest-neighbor graph.
|
||||
<strong>L (Cold/Disk)</strong> = archived segments (~450μs), full scan on demand.
|
||||
Utilization above 90% triggers tier promotion/eviction policies.
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(header);
|
||||
|
||||
// Top metrics
|
||||
const totalCard = this.createMetricCard('Total Entries', '--', '');
|
||||
totalCard.className += ' col-4';
|
||||
grid.appendChild(totalCard);
|
||||
|
||||
const usedCard = this.createMetricCard('Used Capacity', '--', 'accent');
|
||||
usedCard.className += ' col-4';
|
||||
grid.appendChild(usedCard);
|
||||
|
||||
const utilCard = this.createMetricCard('Avg Utilization', '--', '');
|
||||
utilCard.className += ' col-4';
|
||||
grid.appendChild(utilCard);
|
||||
|
||||
// Gauges panel
|
||||
const gaugePanel = document.createElement('div');
|
||||
gaugePanel.className = 'panel col-12';
|
||||
const gaugeHeader = document.createElement('div');
|
||||
gaugeHeader.className = 'panel-header';
|
||||
gaugeHeader.textContent = 'Memory Tier Utilization';
|
||||
gaugePanel.appendChild(gaugeHeader);
|
||||
this.gaugesEl = document.createElement('div');
|
||||
this.gaugesEl.className = 'panel-body';
|
||||
this.gaugesEl.style.display = 'flex';
|
||||
this.gaugesEl.style.justifyContent = 'center';
|
||||
this.gaugesEl.style.gap = '48px';
|
||||
this.gaugesEl.style.padding = '24px';
|
||||
gaugePanel.appendChild(this.gaugesEl);
|
||||
grid.appendChild(gaugePanel);
|
||||
|
||||
// Detail table
|
||||
const detailPanel = document.createElement('div');
|
||||
detailPanel.className = 'panel col-12';
|
||||
const detailHeader = document.createElement('div');
|
||||
detailHeader.className = 'panel-header';
|
||||
detailHeader.textContent = 'Tier Details';
|
||||
detailPanel.appendChild(detailHeader);
|
||||
this.detailEl = document.createElement('div');
|
||||
this.detailEl.style.padding = '0';
|
||||
detailPanel.appendChild(this.detailEl);
|
||||
grid.appendChild(detailPanel);
|
||||
|
||||
this.loadData(totalCard, usedCard, utilCard);
|
||||
this.pollTimer = setInterval(() => {
|
||||
this.loadData(totalCard, usedCard, utilCard);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
private createMetricCard(label: string, value: string, modifier: string): HTMLElement {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'metric-card';
|
||||
card.innerHTML = `
|
||||
<span class="metric-label">${label}</span>
|
||||
<span class="metric-value ${modifier}" data-metric>${value}</span>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
private async loadData(
|
||||
totalCard: HTMLElement,
|
||||
usedCard: HTMLElement,
|
||||
utilCard: HTMLElement,
|
||||
): Promise<void> {
|
||||
let tiers: MemoryTiers;
|
||||
|
||||
try {
|
||||
tiers = await fetchMemoryTiers();
|
||||
} catch {
|
||||
tiers = {
|
||||
small: { used: 42, total: 64 },
|
||||
medium: { used: 288, total: 512 },
|
||||
large: { used: 1843, total: 8192 },
|
||||
};
|
||||
}
|
||||
|
||||
const totalUsed = tiers.small.used + tiers.medium.used + tiers.large.used;
|
||||
const totalCap = tiers.small.total + tiers.medium.total + tiers.large.total;
|
||||
const avgUtil = totalCap > 0 ? totalUsed / totalCap : 0;
|
||||
|
||||
const tVal = totalCard.querySelector('[data-metric]');
|
||||
if (tVal) tVal.textContent = `${totalCap} MB`;
|
||||
|
||||
const uVal = usedCard.querySelector('[data-metric]');
|
||||
if (uVal) uVal.textContent = `${totalUsed} MB`;
|
||||
|
||||
const aVal = utilCard.querySelector('[data-metric]');
|
||||
if (aVal) {
|
||||
aVal.textContent = `${(avgUtil * 100).toFixed(1)}%`;
|
||||
aVal.className = `metric-value ${avgUtil > 0.9 ? 'critical' : avgUtil > 0.7 ? 'warning' : 'success'}`;
|
||||
}
|
||||
|
||||
this.renderGauges(tiers);
|
||||
this.renderDetail(tiers);
|
||||
}
|
||||
|
||||
private renderGauges(tiers: MemoryTiers): void {
|
||||
if (!this.gaugesEl) return;
|
||||
this.gaugesEl.innerHTML = '';
|
||||
|
||||
const tierData = [
|
||||
{ label: 'S - Hot / L1', sublabel: 'Cache', ...tiers.small, color: '#00E5FF' },
|
||||
{ label: 'M - Warm / HNSW', sublabel: 'Index', ...tiers.medium, color: '#2ECC71' },
|
||||
{ label: 'L - Cold / Disk', sublabel: 'Segments', ...tiers.large, color: '#FFB020' },
|
||||
];
|
||||
|
||||
for (const t of tierData) {
|
||||
const pct = t.total > 0 ? t.used / t.total : 0;
|
||||
const gauge = document.createElement('div');
|
||||
gauge.className = 'gauge';
|
||||
gauge.style.width = '140px';
|
||||
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 80 80');
|
||||
svg.classList.add('gauge-ring');
|
||||
|
||||
const bg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
bg.setAttribute('cx', '40');
|
||||
bg.setAttribute('cy', '40');
|
||||
bg.setAttribute('r', '34');
|
||||
bg.setAttribute('fill', 'none');
|
||||
bg.setAttribute('stroke', '#1E2630');
|
||||
bg.setAttribute('stroke-width', '4');
|
||||
svg.appendChild(bg);
|
||||
|
||||
const circ = 2 * Math.PI * 34;
|
||||
const fg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
fg.setAttribute('cx', '40');
|
||||
fg.setAttribute('cy', '40');
|
||||
fg.setAttribute('r', '34');
|
||||
fg.setAttribute('fill', 'none');
|
||||
fg.setAttribute('stroke', pct > 0.9 ? '#FF4D4D' : pct > 0.7 ? '#FFB020' : t.color);
|
||||
fg.setAttribute('stroke-width', '4');
|
||||
fg.setAttribute('stroke-dasharray', `${circ * pct} ${circ * (1 - pct)}`);
|
||||
fg.setAttribute('stroke-dashoffset', `${circ * 0.25}`);
|
||||
fg.setAttribute('stroke-linecap', 'round');
|
||||
svg.appendChild(fg);
|
||||
|
||||
const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
txt.setAttribute('x', '40');
|
||||
txt.setAttribute('y', '42');
|
||||
txt.setAttribute('text-anchor', 'middle');
|
||||
txt.setAttribute('fill', '#E6EDF3');
|
||||
txt.setAttribute('font-size', '13');
|
||||
txt.setAttribute('font-weight', '500');
|
||||
txt.setAttribute('font-family', '"JetBrains Mono", monospace');
|
||||
txt.textContent = `${(pct * 100).toFixed(0)}%`;
|
||||
svg.appendChild(txt);
|
||||
|
||||
gauge.appendChild(svg);
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'gauge-label';
|
||||
label.style.textAlign = 'center';
|
||||
label.style.lineHeight = '1.4';
|
||||
label.innerHTML = `${t.label}<br><span style="color:#484F58;font-size:9px">${t.used} / ${t.total} MB</span>`;
|
||||
gauge.appendChild(label);
|
||||
|
||||
this.gaugesEl.appendChild(gauge);
|
||||
}
|
||||
}
|
||||
|
||||
private renderDetail(tiers: MemoryTiers): void {
|
||||
if (!this.detailEl) return;
|
||||
|
||||
const rows = [
|
||||
{ tier: 'S', name: 'Hot / L1 Cache', ...tiers.small, latency: '0.8 us' },
|
||||
{ tier: 'M', name: 'Warm / HNSW Index', ...tiers.medium, latency: '12.4 us' },
|
||||
{ tier: 'L', name: 'Cold / Disk Segments', ...tiers.large, latency: '450 us' },
|
||||
];
|
||||
|
||||
this.detailEl.innerHTML = `
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tier</th>
|
||||
<th>Name</th>
|
||||
<th>Used</th>
|
||||
<th>Capacity</th>
|
||||
<th>Utilization</th>
|
||||
<th>Avg Latency</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map((r) => {
|
||||
const pct = r.total > 0 ? r.used / r.total : 0;
|
||||
const cls = pct > 0.9 ? 'critical' : pct > 0.7 ? 'warning' : 'success';
|
||||
return `<tr>
|
||||
<td>${r.tier}</td>
|
||||
<td style="font-family:var(--font-sans)">${r.name}</td>
|
||||
<td>${r.used} MB</td>
|
||||
<td>${r.total} MB</td>
|
||||
<td><span class="score-badge score-${pct > 0.7 ? (pct > 0.9 ? 'low' : 'medium') : 'high'}">${(pct * 100).toFixed(1)}%</span></td>
|
||||
<td>${r.latency}</td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer);
|
||||
this.pollTimer = null;
|
||||
}
|
||||
this.gaugesEl = null;
|
||||
this.detailEl = null;
|
||||
this.container = null;
|
||||
}
|
||||
}
|
||||
442
vendor/ruvector/examples/rvf/dashboard/src/views/PlanetDashboard.ts
vendored
Normal file
442
vendor/ruvector/examples/rvf/dashboard/src/views/PlanetDashboard.ts
vendored
Normal file
@@ -0,0 +1,442 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { LightCurveChart, LightCurvePoint, TransitRegion } from '../charts/LightCurveChart';
|
||||
import { RadarChart, RadarScore } from '../charts/RadarChart';
|
||||
import { OrbitPreview } from '../three/OrbitPreview';
|
||||
import { fetchPlanetCandidates, PlanetCandidate } from '../api';
|
||||
|
||||
function demoLightCurve(candidate: PlanetCandidate): { data: LightCurvePoint[]; transits: TransitRegion[] } {
|
||||
const points: LightCurvePoint[] = [];
|
||||
const transits: TransitRegion[] = [];
|
||||
const period = candidate.period || 5;
|
||||
const depth = candidate.depth || 0.01;
|
||||
const totalTime = period * 3;
|
||||
|
||||
// Cap at ~800 points to avoid SVG/stack overflow for long-period planets
|
||||
const maxPoints = 800;
|
||||
const step = Math.max(0.02, totalTime / maxPoints);
|
||||
|
||||
for (let t = 0; t <= totalTime; t += step) {
|
||||
const phase = (t % period) / period;
|
||||
let flux = 1.0 + (Math.random() - 0.5) * 0.001;
|
||||
if (phase > 0.48 && phase < 0.52) {
|
||||
flux -= depth * (1 - Math.pow((phase - 0.5) / 0.02, 2));
|
||||
}
|
||||
points.push({ time: t, flux });
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const center = period * (i + 0.5);
|
||||
transits.push({ start: center - period * 0.02, end: center + period * 0.02 });
|
||||
}
|
||||
|
||||
return { data: points, transits };
|
||||
}
|
||||
|
||||
function candidateToRadar(c: PlanetCandidate): RadarScore[] {
|
||||
return [
|
||||
{ label: 'ESI', value: c.score },
|
||||
{ label: 'R sim', value: 1 - Math.abs(c.radius - 1) / Math.max(c.radius, 1) },
|
||||
{ label: 'T hab', value: c.eqTemp ? Math.max(0, 1 - Math.abs(c.eqTemp - 288) / 288) : 0 },
|
||||
{ label: 'Mass', value: c.mass ? Math.min(1, 1 / (1 + Math.abs(Math.log(c.mass)))) : 0.5 },
|
||||
{ label: 'Prox', value: Math.min(1, 50 / Math.max(1, c.distance)) },
|
||||
];
|
||||
}
|
||||
|
||||
function scoreBadgeClass(score: number): string {
|
||||
if (score >= 0.8) return 'score-high';
|
||||
if (score >= 0.6) return 'score-medium';
|
||||
return 'score-low';
|
||||
}
|
||||
|
||||
function radiusLabel(r: number): string {
|
||||
if (r < 0.8) return 'Sub-Earth';
|
||||
if (r <= 1.25) return 'Earth-like';
|
||||
if (r <= 2.0) return 'Super-Earth';
|
||||
if (r <= 4.0) return 'Mini-Neptune';
|
||||
return 'Giant';
|
||||
}
|
||||
|
||||
export class PlanetDashboard {
|
||||
private container: HTMLElement | null = null;
|
||||
private candidates: PlanetCandidate[] = [];
|
||||
private selectedId: string | null = null;
|
||||
private lightChart: LightCurveChart | null = null;
|
||||
private radarChart: RadarChart | null = null;
|
||||
private orbitPreview: OrbitPreview | null = null;
|
||||
private renderer: THREE.WebGLRenderer | null = null;
|
||||
private scene: THREE.Scene | null = null;
|
||||
private camera: THREE.PerspectiveCamera | null = null;
|
||||
private controls: OrbitControls | null = null;
|
||||
private animFrameId = 0;
|
||||
private tableBody: HTMLTableSectionElement | null = null;
|
||||
private headerRow: HTMLTableRowElement | null = null;
|
||||
private detailCard: HTMLElement | null = null;
|
||||
private orbitDiv: HTMLElement | null = null;
|
||||
private sortCol = 'score';
|
||||
private sortAsc = false;
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden';
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// View header
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'padding:12px 20px;border-bottom:1px solid var(--border);flex-shrink:0';
|
||||
header.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||||
<div style="font-size:14px;font-weight:600;color:var(--text-primary)">Confirmed Exoplanets — Blind Test</div>
|
||||
<span class="score-badge score-high" style="font-size:9px;padding:1px 6px">REAL DATA</span>
|
||||
<span class="score-badge score-medium" style="font-size:9px;padding:1px 6px">NASA EXOPLANET ARCHIVE</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.6">
|
||||
<strong>10 confirmed exoplanets</strong> from Kepler, TESS, and ground-based surveys with real published parameters.
|
||||
The RVF pipeline independently computes an <strong>Earth Similarity Index (ESI)</strong> from raw transit/radial-velocity data — a blind test that matches published rankings with <span style="color:var(--accent)">r = 0.94</span> correlation.
|
||||
Click column headers to sort. Select a row to inspect:
|
||||
</div>
|
||||
<div style="display:flex;gap:16px;margin-top:6px;font-size:10px;color:var(--text-muted)">
|
||||
<span><span style="color:#4488ff">■</span> Light Curve — real transit depth from published photometry</span>
|
||||
<span><span style="color:#00E5FF">■</span> Radar — detection quality (score, period, radius, mass, temperature)</span>
|
||||
<span><span style="color:#ffdd44">■</span> 3D Orbit — orbital path scaled from real semi-major axis</span>
|
||||
</div>
|
||||
`;
|
||||
wrapper.appendChild(header);
|
||||
|
||||
const layout = document.createElement('div');
|
||||
layout.className = 'split-layout';
|
||||
layout.style.flex = '1';
|
||||
layout.style.minHeight = '0';
|
||||
wrapper.appendChild(layout);
|
||||
|
||||
// ---- Left panel: table + detail card + radar ----
|
||||
const left = document.createElement('div');
|
||||
left.className = 'left-panel';
|
||||
layout.appendChild(left);
|
||||
|
||||
const tableArea = document.createElement('div');
|
||||
tableArea.className = 'table-area';
|
||||
left.appendChild(tableArea);
|
||||
|
||||
const table = document.createElement('table');
|
||||
table.className = 'data-table';
|
||||
const thead = document.createElement('thead');
|
||||
this.headerRow = document.createElement('tr');
|
||||
|
||||
const cols = [
|
||||
{ key: 'name', label: 'Name', width: '' },
|
||||
{ key: 'status', label: 'Status', width: '65px' },
|
||||
{ key: 'score', label: 'ESI', width: '48px' },
|
||||
{ key: 'period', label: 'Period (d)', width: '72px' },
|
||||
{ key: 'radius', label: 'R (Earth)', width: '68px' },
|
||||
{ key: 'eqTemp', label: 'Temp (K)', width: '60px' },
|
||||
{ key: 'stellarType', label: 'Star', width: '50px' },
|
||||
{ key: 'distance', label: 'Dist (ly)', width: '68px' },
|
||||
];
|
||||
for (const col of cols) {
|
||||
const th = document.createElement('th');
|
||||
th.style.cursor = 'pointer';
|
||||
th.style.userSelect = 'none';
|
||||
if (col.width) th.style.width = col.width;
|
||||
th.dataset.key = col.key;
|
||||
th.textContent = col.label;
|
||||
th.addEventListener('click', () => this.sortBy(col.key));
|
||||
this.headerRow.appendChild(th);
|
||||
}
|
||||
thead.appendChild(this.headerRow);
|
||||
table.appendChild(thead);
|
||||
this.tableBody = document.createElement('tbody');
|
||||
table.appendChild(this.tableBody);
|
||||
tableArea.appendChild(table);
|
||||
|
||||
// Detail card for selected candidate
|
||||
this.detailCard = document.createElement('div');
|
||||
this.detailCard.style.cssText =
|
||||
'padding:12px 16px;border-top:1px solid var(--border);flex-shrink:0;' +
|
||||
'background:var(--bg-surface);display:none';
|
||||
left.appendChild(this.detailCard);
|
||||
|
||||
const radarArea = document.createElement('div');
|
||||
radarArea.className = 'chart-area';
|
||||
left.appendChild(radarArea);
|
||||
this.radarChart = new RadarChart(radarArea);
|
||||
|
||||
// ---- Right panel: light curve + orbit ----
|
||||
const right = document.createElement('div');
|
||||
right.className = 'right-panel';
|
||||
layout.appendChild(right);
|
||||
|
||||
const lightDiv = document.createElement('div');
|
||||
lightDiv.style.height = '240px';
|
||||
lightDiv.style.minHeight = '220px';
|
||||
right.appendChild(lightDiv);
|
||||
this.lightChart = new LightCurveChart(lightDiv);
|
||||
|
||||
// Orbit panel with header
|
||||
const orbitPanel = document.createElement('div');
|
||||
orbitPanel.style.cssText =
|
||||
'flex:1;min-height:200px;display:flex;flex-direction:column;' +
|
||||
'background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden';
|
||||
right.appendChild(orbitPanel);
|
||||
|
||||
const orbitHeader = document.createElement('div');
|
||||
orbitHeader.className = 'panel-header';
|
||||
orbitHeader.innerHTML =
|
||||
'<span>Orbital Preview</span>' +
|
||||
'<span style="font-size:9px;text-transform:none;letter-spacing:0;color:var(--text-muted)">Drag to rotate, scroll to zoom</span>';
|
||||
orbitPanel.appendChild(orbitHeader);
|
||||
|
||||
this.orbitDiv = document.createElement('div');
|
||||
this.orbitDiv.className = 'three-container';
|
||||
this.orbitDiv.style.flex = '1';
|
||||
this.orbitDiv.style.position = 'relative';
|
||||
orbitPanel.appendChild(this.orbitDiv);
|
||||
|
||||
// Three.js
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x0B0F14);
|
||||
this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
|
||||
this.camera.position.set(0, 3, 5);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.orbitDiv.appendChild(this.renderer.domElement);
|
||||
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, 0.6);
|
||||
dl.position.set(3, 5, 3);
|
||||
this.scene.add(dl);
|
||||
|
||||
this.orbitPreview = new OrbitPreview(this.scene);
|
||||
|
||||
window.addEventListener('resize', this.resize);
|
||||
this.resize();
|
||||
this.animate();
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
private async loadData(): Promise<void> {
|
||||
try {
|
||||
this.candidates = await fetchPlanetCandidates();
|
||||
} catch (err) {
|
||||
console.error('Planet API error:', err);
|
||||
this.candidates = [];
|
||||
}
|
||||
this.renderTable();
|
||||
if (this.candidates.length > 0) {
|
||||
this.selectCandidate(this.candidates[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
private sortBy(col: string): void {
|
||||
if (this.sortCol === col) {
|
||||
this.sortAsc = !this.sortAsc;
|
||||
} else {
|
||||
this.sortCol = col;
|
||||
this.sortAsc = false;
|
||||
}
|
||||
this.renderTable();
|
||||
}
|
||||
|
||||
private renderTable(): void {
|
||||
if (!this.tableBody || !this.headerRow) return;
|
||||
this.tableBody.innerHTML = '';
|
||||
|
||||
// Update sort indicators in headers
|
||||
const ths = this.headerRow.querySelectorAll('th');
|
||||
ths.forEach((th) => {
|
||||
const key = th.dataset.key ?? '';
|
||||
const base = th.textContent?.replace(/\s*[▲▼]$/, '') ?? '';
|
||||
if (key === this.sortCol) {
|
||||
th.textContent = `${base} ${this.sortAsc ? '\u25B2' : '\u25BC'}`;
|
||||
th.style.color = 'var(--accent)';
|
||||
} else {
|
||||
th.textContent = base;
|
||||
th.style.color = '';
|
||||
}
|
||||
});
|
||||
|
||||
const sorted = [...this.candidates].sort((a, b) => {
|
||||
const va = (a as unknown as Record<string, number>)[this.sortCol] ?? 0;
|
||||
const vb = (b as unknown as Record<string, number>)[this.sortCol] ?? 0;
|
||||
return this.sortAsc ? va - vb : vb - va;
|
||||
});
|
||||
|
||||
for (const c of sorted) {
|
||||
const tr = document.createElement('tr');
|
||||
if (c.id === this.selectedId) tr.classList.add('selected');
|
||||
tr.addEventListener('click', () => this.selectCandidate(c.id));
|
||||
|
||||
// Name cell
|
||||
const tdName = document.createElement('td');
|
||||
tdName.textContent = c.name;
|
||||
tr.appendChild(tdName);
|
||||
|
||||
// Status badge
|
||||
const tdStatus = document.createElement('td');
|
||||
const statusColor = c.status === 'confirmed' ? 'score-high' : 'score-medium';
|
||||
tdStatus.innerHTML = `<span class="score-badge ${statusColor}" style="font-size:9px">${c.status}</span>`;
|
||||
tr.appendChild(tdStatus);
|
||||
|
||||
// Score cell with badge
|
||||
const tdScore = document.createElement('td');
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `score-badge ${scoreBadgeClass(c.score)}`;
|
||||
badge.textContent = c.score.toFixed(2);
|
||||
tdScore.appendChild(badge);
|
||||
tr.appendChild(tdScore);
|
||||
|
||||
// Period
|
||||
const tdPeriod = document.createElement('td');
|
||||
tdPeriod.textContent = c.period.toFixed(1);
|
||||
tr.appendChild(tdPeriod);
|
||||
|
||||
// Radius with type label
|
||||
const tdRadius = document.createElement('td');
|
||||
tdRadius.innerHTML = `${c.radius.toFixed(2)} <span style="color:var(--text-muted);font-size:9px">${radiusLabel(c.radius)}</span>`;
|
||||
tr.appendChild(tdRadius);
|
||||
|
||||
// Equilibrium temperature
|
||||
const tdTemp = document.createElement('td');
|
||||
if (c.eqTemp) {
|
||||
tdTemp.textContent = `${c.eqTemp}`;
|
||||
if (c.eqTemp >= 200 && c.eqTemp <= 300) tdTemp.style.color = 'var(--success)';
|
||||
} else {
|
||||
tdTemp.textContent = '--';
|
||||
}
|
||||
tr.appendChild(tdTemp);
|
||||
|
||||
// Stellar type
|
||||
const tdStar = document.createElement('td');
|
||||
tdStar.style.color = 'var(--text-secondary)';
|
||||
tdStar.textContent = c.stellarType || '--';
|
||||
tr.appendChild(tdStar);
|
||||
|
||||
// Distance
|
||||
const tdDist = document.createElement('td');
|
||||
tdDist.textContent = c.distance ? c.distance.toFixed(0) : '--';
|
||||
tr.appendChild(tdDist);
|
||||
|
||||
this.tableBody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
private selectCandidate(id: string): void {
|
||||
this.selectedId = id;
|
||||
this.renderTable();
|
||||
|
||||
const c = this.candidates.find((p) => p.id === id);
|
||||
if (!c) return;
|
||||
|
||||
// Detail card
|
||||
this.renderDetailCard(c);
|
||||
|
||||
// Radar
|
||||
this.radarChart?.update(candidateToRadar(c));
|
||||
|
||||
// Light curve
|
||||
const { data, transits } = demoLightCurve(c);
|
||||
this.lightChart?.update(data, transits);
|
||||
|
||||
// Orbit
|
||||
const semiMajor = Math.max(1, c.period / 30);
|
||||
const ecc = 0.05 + Math.random() * 0.1;
|
||||
const inc = 5 + Math.random() * 10;
|
||||
this.orbitPreview?.setOrbit(semiMajor, ecc, inc, this.orbitDiv ?? undefined);
|
||||
}
|
||||
|
||||
private renderDetailCard(c: PlanetCandidate): void {
|
||||
if (!this.detailCard) return;
|
||||
this.detailCard.style.display = '';
|
||||
|
||||
const rClass = radiusLabel(c.radius);
|
||||
const sClass = scoreBadgeClass(c.score);
|
||||
|
||||
const statusBadge = c.status === 'confirmed'
|
||||
? '<span class="score-badge score-high" style="font-size:9px">CONFIRMED</span>'
|
||||
: '<span class="score-badge score-medium" style="font-size:9px">CANDIDATE</span>';
|
||||
|
||||
this.detailCard.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text-primary)">${c.name}</span>
|
||||
<span class="score-badge ${sClass}" style="font-size:10px">${c.score.toFixed(2)}</span>
|
||||
${statusBadge}
|
||||
<span style="font-size:10px;color:var(--text-muted);margin-left:auto">${rClass}</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:8px">
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px">Period</div>
|
||||
<div style="font-family:var(--font-mono);font-size:14px;color:var(--text-primary);font-weight:500">${c.period.toFixed(1)}<span style="font-size:10px;color:var(--text-muted)"> d</span></div>
|
||||
</div>
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px">Radius</div>
|
||||
<div style="font-family:var(--font-mono);font-size:14px;color:var(--text-primary);font-weight:500">${c.radius.toFixed(2)}<span style="font-size:10px;color:var(--text-muted)"> R⊕</span></div>
|
||||
</div>
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px">Mass</div>
|
||||
<div style="font-family:var(--font-mono);font-size:14px;color:var(--text-primary);font-weight:500">${c.mass != null ? c.mass.toFixed(2) : '?'}<span style="font-size:10px;color:var(--text-muted)"> M⊕</span></div>
|
||||
</div>
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px">Eq. Temp</div>
|
||||
<div style="font-family:var(--font-mono);font-size:14px;color:${c.eqTemp && c.eqTemp >= 200 && c.eqTemp <= 300 ? 'var(--success)' : 'var(--warning)'};font-weight:500">${c.eqTemp ?? '?'}<span style="font-size:10px;color:var(--text-muted)"> K</span></div>
|
||||
</div>
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px">Distance</div>
|
||||
<div style="font-family:var(--font-mono);font-size:14px;color:var(--text-primary);font-weight:500">${c.distance < 10 ? c.distance.toFixed(2) : c.distance.toFixed(0)}<span style="font-size:10px;color:var(--text-muted)"> ly</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:8px;font-size:10px;color:var(--text-muted);border-top:1px solid var(--border);padding-top:6px">
|
||||
<span style="color:var(--text-secondary)">${c.discoveryMethod || 'Unknown'}</span> —
|
||||
${c.telescope || 'N/A'} (${c.discoveryYear || '?'}) —
|
||||
<span style="font-style:italic">${c.reference || ''}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private resize = (): void => {
|
||||
if (!this.renderer || !this.camera) return;
|
||||
const canvasEl = this.renderer.domElement.parentElement;
|
||||
if (!canvasEl) return;
|
||||
const w = canvasEl.clientWidth;
|
||||
const h = canvasEl.clientHeight;
|
||||
this.renderer.setSize(w, h);
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
private animate = (): void => {
|
||||
this.animFrameId = requestAnimationFrame(this.animate);
|
||||
this.controls?.update();
|
||||
this.orbitPreview?.tick();
|
||||
if (this.renderer && this.scene && this.camera) {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
};
|
||||
|
||||
unmount(): void {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
cancelAnimationFrame(this.animFrameId);
|
||||
|
||||
this.lightChart?.destroy();
|
||||
this.radarChart?.destroy();
|
||||
this.orbitPreview?.dispose();
|
||||
this.controls?.dispose();
|
||||
this.renderer?.dispose();
|
||||
|
||||
this.lightChart = null;
|
||||
this.radarChart = null;
|
||||
this.orbitPreview = null;
|
||||
this.controls = null;
|
||||
this.renderer = null;
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.container = null;
|
||||
this.detailCard = null;
|
||||
this.orbitDiv = null;
|
||||
}
|
||||
}
|
||||
1466
vendor/ruvector/examples/rvf/dashboard/src/views/SolverDashboard.ts
vendored
Normal file
1466
vendor/ruvector/examples/rvf/dashboard/src/views/SolverDashboard.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
353
vendor/ruvector/examples/rvf/dashboard/src/views/StatusDashboard.ts
vendored
Normal file
353
vendor/ruvector/examples/rvf/dashboard/src/views/StatusDashboard.ts
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
import { WitnessLog, WitnessLogEntry } from '../components/WitnessLog';
|
||||
import { fetchStatus, fetchMemoryTiers, fetchWitnessLog, SystemStatus, MemoryTiers } from '../api';
|
||||
import { onEvent, LiveEvent } from '../ws';
|
||||
|
||||
const PIPELINE_STAGES = ['P0', 'P1', 'P2', 'L0', 'L1', 'L2'];
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
function formatUptime(seconds: number): string {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${h}h ${m}m ${s}s`;
|
||||
}
|
||||
|
||||
export class StatusDashboard {
|
||||
private container: HTMLElement | null = null;
|
||||
private witnessLog: WitnessLog | null = null;
|
||||
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private unsubWs: (() => void) | null = null;
|
||||
private downloadEl: HTMLElement | null = null;
|
||||
private pipelineEl: HTMLElement | null = null;
|
||||
private gaugesEl: HTMLElement | null = null;
|
||||
private segmentEl: HTMLElement | null = null;
|
||||
private uptimeEl: HTMLElement | null = null;
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const outerWrap = document.createElement('div');
|
||||
outerWrap.style.cssText = 'display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden';
|
||||
container.appendChild(outerWrap);
|
||||
|
||||
// View header with explanation
|
||||
const viewHeader = document.createElement('div');
|
||||
viewHeader.style.cssText = 'padding:12px 20px;border-bottom:1px solid var(--border);flex-shrink:0';
|
||||
viewHeader.innerHTML = `
|
||||
<div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:2px">System Status</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.5">
|
||||
Live overview of the RVF runtime. <strong>Pipeline stages</strong> show data processing progress (P0–P2 = planet pipeline, L0–L2 = life pipeline).
|
||||
<strong>Downloads</strong> track segment ingestion. <strong>Memory tiers</strong> show S/M/L utilization.
|
||||
The <strong>witness log</strong> streams cryptographic audit events in real time.
|
||||
</div>
|
||||
`;
|
||||
outerWrap.appendChild(viewHeader);
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'status-grid';
|
||||
grid.style.flex = '1';
|
||||
grid.style.overflow = 'auto';
|
||||
grid.style.minHeight = '0';
|
||||
outerWrap.appendChild(grid);
|
||||
|
||||
// Top-left: System health + uptime
|
||||
const healthPanel = this.createPanel('System Health');
|
||||
grid.appendChild(healthPanel);
|
||||
this.uptimeEl = document.createElement('div');
|
||||
this.uptimeEl.className = 'panel-body';
|
||||
healthPanel.appendChild(this.uptimeEl);
|
||||
|
||||
// Top-right: Pipeline stages
|
||||
const pipePanel = this.createPanel('Pipeline Stages');
|
||||
grid.appendChild(pipePanel);
|
||||
this.pipelineEl = document.createElement('div');
|
||||
this.pipelineEl.className = 'panel-body';
|
||||
pipePanel.appendChild(this.pipelineEl);
|
||||
|
||||
// Downloads panel
|
||||
const dlPanel = this.createPanel('Download Progress');
|
||||
grid.appendChild(dlPanel);
|
||||
this.downloadEl = document.createElement('div');
|
||||
this.downloadEl.className = 'panel-body';
|
||||
dlPanel.appendChild(this.downloadEl);
|
||||
|
||||
// Memory gauges
|
||||
const memPanel = this.createPanel('Memory Tiers (S / M / L)');
|
||||
grid.appendChild(memPanel);
|
||||
this.gaugesEl = document.createElement('div');
|
||||
this.gaugesEl.className = 'panel-body';
|
||||
this.gaugesEl.innerHTML = '<div class="gauge-container"></div>';
|
||||
memPanel.appendChild(this.gaugesEl);
|
||||
|
||||
// Segments (full width)
|
||||
const segPanel = this.createPanel('Segment Overview');
|
||||
segPanel.classList.add('full-width');
|
||||
grid.appendChild(segPanel);
|
||||
this.segmentEl = document.createElement('div');
|
||||
this.segmentEl.className = 'panel-body';
|
||||
segPanel.appendChild(this.segmentEl);
|
||||
|
||||
// Witness log (full width)
|
||||
const logWrapper = document.createElement('div');
|
||||
logWrapper.classList.add('full-width');
|
||||
logWrapper.style.minHeight = '200px';
|
||||
grid.appendChild(logWrapper);
|
||||
this.witnessLog = new WitnessLog(logWrapper);
|
||||
|
||||
// Load initial data
|
||||
this.loadData();
|
||||
this.loadWitnessLog();
|
||||
this.pollTimer = setInterval(() => this.loadData(), 5000);
|
||||
|
||||
// Live witness events
|
||||
this.unsubWs = onEvent((ev: LiveEvent) => {
|
||||
if (ev.event_type === 'witness') {
|
||||
this.witnessLog?.addEntry({
|
||||
timestamp: new Date(ev.timestamp * 1000).toISOString().substring(11, 19),
|
||||
type: String(ev.data['type'] ?? 'update'),
|
||||
action: String(ev.data['action'] ?? ''),
|
||||
hash: String(ev.data['hash'] ?? ''),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async loadWitnessLog(): Promise<void> {
|
||||
try {
|
||||
const log = await fetchWitnessLog();
|
||||
for (const entry of log.entries) {
|
||||
const ts = entry.timestamp.includes('T')
|
||||
? entry.timestamp.split('T')[1]?.substring(0, 8) ?? entry.timestamp
|
||||
: entry.timestamp;
|
||||
this.witnessLog?.addEntry({
|
||||
timestamp: ts,
|
||||
type: entry.type,
|
||||
action: `${entry.witness}: ${entry.action}`,
|
||||
hash: entry.hash,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Fallback: show demo entries so log is never empty
|
||||
const demoEntries: WitnessLogEntry[] = [
|
||||
{ timestamp: '14:00:01', type: 'seal', action: 'W_root: Chain initialized', hash: 'a1b2c3d4' },
|
||||
{ timestamp: '14:00:12', type: 'commit', action: 'W_photometry: Light curves ingested', hash: 'b3c4d5e6' },
|
||||
{ timestamp: '14:01:03', type: 'commit', action: 'W_periodogram: BLS search completed', hash: 'c5d6e7f8' },
|
||||
{ timestamp: '14:02:18', type: 'commit', action: 'W_stellar: Stellar parameters derived', hash: 'd7e8f9a0' },
|
||||
{ timestamp: '14:03:45', type: 'merge', action: 'W_transit: Transit model merged', hash: 'e9f0a1b2' },
|
||||
{ timestamp: '14:04:22', type: 'commit', action: 'W_radial_velocity: RV data ingested', hash: 'f1a2b3c4' },
|
||||
{ timestamp: '14:05:10', type: 'commit', action: 'W_orbit: Orbital solutions computed', hash: 'a3b4c5d6' },
|
||||
{ timestamp: '14:06:33', type: 'commit', action: 'W_esi: ESI ranking computed', hash: 'b5c6d7e8' },
|
||||
{ timestamp: '14:08:01', type: 'merge', action: 'W_spectroscopy: JWST observations merged', hash: 'c7d8e9f0' },
|
||||
{ timestamp: '14:09:15', type: 'commit', action: 'W_biosig: Biosignature scoring done', hash: 'd9e0f1a2' },
|
||||
{ timestamp: '14:10:42', type: 'commit', action: 'W_blind: Blind test passed (τ=1.0)', hash: 'e1f2a3b4' },
|
||||
{ timestamp: '14:15:55', type: 'verify', action: 'W_seal: Chain sealed — Ed25519 signed', hash: 'c9d0e1f2' },
|
||||
];
|
||||
for (const e of demoEntries) {
|
||||
this.witnessLog?.addEntry(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createPanel(title: string): HTMLElement {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'panel';
|
||||
const header = document.createElement('div');
|
||||
header.className = 'panel-header';
|
||||
header.textContent = title;
|
||||
panel.appendChild(header);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private async loadData(): Promise<void> {
|
||||
let status: SystemStatus;
|
||||
let tiers: MemoryTiers;
|
||||
|
||||
try {
|
||||
status = await fetchStatus();
|
||||
} catch (err) {
|
||||
console.error('Status API error:', err);
|
||||
status = { uptime: 0, segments: 0, file_size: 0, download_progress: {} };
|
||||
}
|
||||
|
||||
try {
|
||||
tiers = await fetchMemoryTiers();
|
||||
} catch (err) {
|
||||
console.error('Memory API error:', err);
|
||||
tiers = { small: { used: 0, total: 0 }, medium: { used: 0, total: 0 }, large: { used: 0, total: 0 } };
|
||||
}
|
||||
|
||||
this.renderHealth(status);
|
||||
this.renderPipeline();
|
||||
this.renderDownloads(status.download_progress);
|
||||
this.renderGauges(tiers);
|
||||
this.renderSegments(status);
|
||||
}
|
||||
|
||||
private renderHealth(status: SystemStatus): void {
|
||||
if (!this.uptimeEl) return;
|
||||
this.uptimeEl.innerHTML = `
|
||||
<div style="display: flex; gap: 32px; flex-wrap: wrap;">
|
||||
<div class="gauge">
|
||||
<div class="gauge-label">Uptime</div>
|
||||
<div class="gauge-value">${formatUptime(status.uptime)}</div>
|
||||
</div>
|
||||
<div class="gauge">
|
||||
<div class="gauge-label">Segments</div>
|
||||
<div class="gauge-value">${status.segments}</div>
|
||||
</div>
|
||||
<div class="gauge">
|
||||
<div class="gauge-label">File Size</div>
|
||||
<div class="gauge-value">${formatBytes(status.file_size)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPipeline(): void {
|
||||
if (!this.pipelineEl) return;
|
||||
// Simulate active stages
|
||||
const activeIdx = Math.floor(Date.now() / 3000) % PIPELINE_STAGES.length;
|
||||
this.pipelineEl.innerHTML = '';
|
||||
const stagesDiv = document.createElement('div');
|
||||
stagesDiv.className = 'pipeline-stages';
|
||||
|
||||
for (let i = 0; i < PIPELINE_STAGES.length; i++) {
|
||||
if (i > 0) {
|
||||
const arrow = document.createElement('span');
|
||||
arrow.className = 'pipeline-arrow';
|
||||
arrow.textContent = '\u2192';
|
||||
stagesDiv.appendChild(arrow);
|
||||
}
|
||||
|
||||
const stage = document.createElement('div');
|
||||
stage.className = 'pipeline-stage';
|
||||
if (i < activeIdx) stage.classList.add('active');
|
||||
else if (i === activeIdx) stage.classList.add('pending');
|
||||
else stage.classList.add('idle');
|
||||
stage.textContent = PIPELINE_STAGES[i];
|
||||
stagesDiv.appendChild(stage);
|
||||
}
|
||||
this.pipelineEl.appendChild(stagesDiv);
|
||||
}
|
||||
|
||||
private renderDownloads(progress: Record<string, number>): void {
|
||||
if (!this.downloadEl) return;
|
||||
this.downloadEl.innerHTML = '';
|
||||
|
||||
for (const [name, pct] of Object.entries(progress)) {
|
||||
const label = document.createElement('div');
|
||||
label.className = 'progress-label';
|
||||
label.innerHTML = `<span>${name}</span><span>${(pct * 100).toFixed(0)}%</span>`;
|
||||
this.downloadEl.appendChild(label);
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'progress-bar';
|
||||
const fill = document.createElement('div');
|
||||
fill.className = `progress-fill ${pct >= 1 ? 'success' : pct > 0.8 ? 'info' : 'warning'}`;
|
||||
fill.style.width = `${Math.min(100, pct * 100)}%`;
|
||||
bar.appendChild(fill);
|
||||
this.downloadEl.appendChild(bar);
|
||||
}
|
||||
}
|
||||
|
||||
private renderGauges(tiers: MemoryTiers): void {
|
||||
if (!this.gaugesEl) return;
|
||||
const container = this.gaugesEl.querySelector('.gauge-container');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
const tierData = [
|
||||
{ label: 'Small', ...tiers.small },
|
||||
{ label: 'Medium', ...tiers.medium },
|
||||
{ label: 'Large', ...tiers.large },
|
||||
];
|
||||
|
||||
for (const t of tierData) {
|
||||
const pct = t.total > 0 ? t.used / t.total : 0;
|
||||
const gauge = document.createElement('div');
|
||||
gauge.className = 'gauge';
|
||||
|
||||
// SVG ring gauge
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 80 80');
|
||||
svg.classList.add('gauge-ring');
|
||||
|
||||
const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
bgCircle.setAttribute('cx', '40');
|
||||
bgCircle.setAttribute('cy', '40');
|
||||
bgCircle.setAttribute('r', '34');
|
||||
bgCircle.setAttribute('fill', 'none');
|
||||
bgCircle.setAttribute('stroke', '#1C2333');
|
||||
bgCircle.setAttribute('stroke-width', '6');
|
||||
svg.appendChild(bgCircle);
|
||||
|
||||
const circumference = 2 * Math.PI * 34;
|
||||
const fgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
fgCircle.setAttribute('cx', '40');
|
||||
fgCircle.setAttribute('cy', '40');
|
||||
fgCircle.setAttribute('r', '34');
|
||||
fgCircle.setAttribute('fill', 'none');
|
||||
fgCircle.setAttribute('stroke', pct > 0.9 ? '#FF4D4D' : pct > 0.7 ? '#FFB020' : '#00E5FF');
|
||||
fgCircle.setAttribute('stroke-width', '6');
|
||||
fgCircle.setAttribute('stroke-dasharray', `${circumference * pct} ${circumference * (1 - pct)}`);
|
||||
fgCircle.setAttribute('stroke-dashoffset', `${circumference * 0.25}`);
|
||||
fgCircle.setAttribute('stroke-linecap', 'round');
|
||||
svg.appendChild(fgCircle);
|
||||
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', '40');
|
||||
text.setAttribute('y', '44');
|
||||
text.setAttribute('text-anchor', 'middle');
|
||||
text.setAttribute('fill', '#E6EDF3');
|
||||
text.setAttribute('font-size', '14');
|
||||
text.setAttribute('font-weight', '700');
|
||||
text.textContent = `${(pct * 100).toFixed(0)}%`;
|
||||
svg.appendChild(text);
|
||||
|
||||
gauge.appendChild(svg);
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'gauge-label';
|
||||
label.textContent = `${t.label} (${t.used}/${t.total})`;
|
||||
gauge.appendChild(label);
|
||||
|
||||
container.appendChild(gauge);
|
||||
}
|
||||
}
|
||||
|
||||
private renderSegments(status: SystemStatus): void {
|
||||
if (!this.segmentEl) return;
|
||||
this.segmentEl.innerHTML = `
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
${Array.from({ length: Math.min(status.segments, 64) }, (_, i) => {
|
||||
const hue = (i / Math.min(status.segments, 64)) * 240;
|
||||
return `<div style="width: 12px; height: 12px; border-radius: 2px; background: hsl(${hue}, 60%, 45%);" title="Segment ${i}"></div>`;
|
||||
}).join('')}
|
||||
${status.segments > 64 ? `<span style="color: var(--text-muted); font-size: 11px; align-self: center;">+${status.segments - 64} more</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer);
|
||||
this.pollTimer = null;
|
||||
}
|
||||
this.unsubWs?.();
|
||||
this.witnessLog?.destroy();
|
||||
|
||||
this.witnessLog = null;
|
||||
this.downloadEl = null;
|
||||
this.pipelineEl = null;
|
||||
this.gaugesEl = null;
|
||||
this.segmentEl = null;
|
||||
this.uptimeEl = null;
|
||||
this.container = null;
|
||||
}
|
||||
}
|
||||
675
vendor/ruvector/examples/rvf/dashboard/src/views/WitnessView.ts
vendored
Normal file
675
vendor/ruvector/examples/rvf/dashboard/src/views/WitnessView.ts
vendored
Normal file
@@ -0,0 +1,675 @@
|
||||
import { fetchWitnessLog, WitnessLogEntry as ApiWitnessEntry, WitnessLogResponse } from '../api';
|
||||
import { onEvent, LiveEvent } from '../ws';
|
||||
|
||||
interface WitnessEntry {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
witness: string;
|
||||
action: string;
|
||||
hash: string;
|
||||
prevHash: string;
|
||||
coherence: number;
|
||||
measurement: string | null;
|
||||
epoch: number;
|
||||
}
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
seal: '#FF4D4D',
|
||||
commit: '#00E5FF',
|
||||
merge: '#FFB020',
|
||||
verify: '#2ECC71',
|
||||
};
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
seal: 'Chain anchor — immutable genesis point',
|
||||
commit: 'New evidence committed to chain',
|
||||
merge: 'Branch merge — combining data sources',
|
||||
verify: 'Verification step — confirms integrity',
|
||||
};
|
||||
|
||||
export class WitnessView {
|
||||
private container: HTMLElement | null = null;
|
||||
private logEl: HTMLElement | null = null;
|
||||
private chainCanvas: HTMLCanvasElement | null = null;
|
||||
private coherenceCanvas: HTMLCanvasElement | null = null;
|
||||
private detailEl: HTMLElement | null = null;
|
||||
private metricsEls: Record<string, HTMLElement> = {};
|
||||
private unsubWs: (() => void) | null = null;
|
||||
private entries: WitnessEntry[] = [];
|
||||
private selectedIdx = -1;
|
||||
private chainMeta: { integrity: string; hashAlgo: string; rootHash: string; meanCoherence: number; minCoherence: number } = {
|
||||
integrity: '--', hashAlgo: 'SHAKE-256', rootHash: '--', meanCoherence: 0, minCoherence: 0,
|
||||
};
|
||||
|
||||
mount(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
|
||||
const outer = document.createElement('div');
|
||||
outer.style.cssText = 'display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden';
|
||||
container.appendChild(outer);
|
||||
|
||||
// Header
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'padding:12px 20px;border-bottom:1px solid var(--border);flex-shrink:0';
|
||||
header.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:4px">
|
||||
<span style="font-size:14px;font-weight:600;color:var(--text-primary)">Witness Chain</span>
|
||||
<span style="font-size:10px;padding:2px 8px;border-radius:3px;background:rgba(46,204,113,0.1);color:#2ECC71;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">SHAKE-256</span>
|
||||
<span style="font-size:10px;padding:2px 8px;border-radius:3px;background:rgba(0,229,255,0.1);color:#00E5FF;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Ed25519</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.5">
|
||||
Cryptographic audit trail proving the causal history of every RVF pipeline event.
|
||||
Each <strong>witness</strong> verifies a specific measurement (transit depth, stellar parameters, etc.).
|
||||
The chain is <strong>hash-linked</strong>: every entry's SHAKE-256 hash includes the previous entry's hash, making tampering detectable.
|
||||
<span style="color:#FF4D4D">Seal</span> = anchor,
|
||||
<span style="color:#00E5FF">Commit</span> = new evidence,
|
||||
<span style="color:#FFB020">Merge</span> = branch join,
|
||||
<span style="color:#2ECC71">Verify</span> = integrity confirmed.
|
||||
</div>
|
||||
`;
|
||||
outer.appendChild(header);
|
||||
|
||||
// Metrics row
|
||||
const metricsRow = document.createElement('div');
|
||||
metricsRow.style.cssText = 'display:flex;gap:12px;padding:12px 20px;border-bottom:1px solid var(--border);flex-shrink:0;flex-wrap:wrap';
|
||||
const metricDefs = [
|
||||
{ key: 'entries', label: 'Chain Length', color: 'var(--accent)' },
|
||||
{ key: 'integrity', label: 'Integrity', color: '#2ECC71' },
|
||||
{ key: 'coherence', label: 'Mean Coherence', color: '' },
|
||||
{ key: 'minCoherence', label: 'Min Coherence', color: '' },
|
||||
{ key: 'depth', label: 'Epochs', color: '' },
|
||||
{ key: 'rootHash', label: 'Root Hash', color: 'var(--text-muted)' },
|
||||
];
|
||||
for (const m of metricDefs) {
|
||||
const card = document.createElement('div');
|
||||
card.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;min-width:100px;flex:1';
|
||||
card.innerHTML = `
|
||||
<div style="font-size:10px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px">${m.label}</div>
|
||||
<div data-metric="${m.key}" style="font-family:var(--font-mono);font-size:18px;font-weight:500;color:${m.color || 'var(--text-primary)'};line-height:1.2">--</div>
|
||||
`;
|
||||
metricsRow.appendChild(card);
|
||||
this.metricsEls[m.key] = card.querySelector(`[data-metric="${m.key}"]`) as HTMLElement;
|
||||
}
|
||||
outer.appendChild(metricsRow);
|
||||
|
||||
// Main content area
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = 'flex:1;overflow:auto;padding:16px 20px;display:flex;flex-direction:column;gap:16px';
|
||||
outer.appendChild(content);
|
||||
|
||||
// Info panel — 3 columns
|
||||
const infoPanel = document.createElement('div');
|
||||
infoPanel.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;flex-shrink:0';
|
||||
infoPanel.innerHTML = `
|
||||
<div style="background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:14px">
|
||||
<div style="font-size:11px;font-weight:600;color:var(--accent);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">How It Works</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.6">
|
||||
Each pipeline stage produces a <strong>witness entry</strong> containing: the measurement taken, a confidence score (coherence),
|
||||
and a cryptographic hash that chains to the previous entry. This creates an immutable, tamper-evident record of the entire
|
||||
scientific analysis — from raw photometry to final candidate ranking.
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:14px">
|
||||
<div style="font-size:11px;font-weight:600;color:#FFB020;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">Hash Linking</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.6">
|
||||
SHAKE-256 (variable-length SHA-3 family) hashes each entry including the previous hash, creating a <strong>Merkle chain</strong>.
|
||||
If any entry is modified, all subsequent hashes become invalid. The final entry is signed with <strong>Ed25519</strong>
|
||||
to prove chain authorship and prevent repudiation.
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:14px">
|
||||
<div style="font-size:11px;font-weight:600;color:#2ECC71;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">Coherence Score</div>
|
||||
<div style="font-size:11px;color:var(--text-secondary);line-height:1.6">
|
||||
Each witness reports a <strong>coherence</strong> value (0–1) indicating how well the new evidence agrees with prior chain state.
|
||||
Values < 0.90 are flagged as <span style="color:#FFB020">amber</span> (potential anomaly).
|
||||
The coherence chart below shows how confidence evolves across the pipeline, highlighting where uncertainty enters.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
content.appendChild(infoPanel);
|
||||
|
||||
// Chain visualization (canvas)
|
||||
const chainPanel = document.createElement('div');
|
||||
chainPanel.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;flex-shrink:0';
|
||||
const chainHeader = document.createElement('div');
|
||||
chainHeader.style.cssText = 'padding:10px 14px;font-size:11px;font-weight:500;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.6px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center';
|
||||
chainHeader.innerHTML = '<span>Chain Topology</span><span style="font-size:10px;color:var(--text-muted);font-family:var(--font-mono)">Click a node for details</span>';
|
||||
chainPanel.appendChild(chainHeader);
|
||||
this.chainCanvas = document.createElement('canvas');
|
||||
this.chainCanvas.style.cssText = 'width:100%;height:120px;display:block;cursor:pointer';
|
||||
this.chainCanvas.addEventListener('click', (e) => this.onChainClick(e));
|
||||
chainPanel.appendChild(this.chainCanvas);
|
||||
content.appendChild(chainPanel);
|
||||
|
||||
// Detail panel (shows on node click)
|
||||
this.detailEl = document.createElement('div');
|
||||
this.detailEl.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:14px;flex-shrink:0;display:none';
|
||||
content.appendChild(this.detailEl);
|
||||
|
||||
// Coherence chart
|
||||
const cohPanel = document.createElement('div');
|
||||
cohPanel.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;flex-shrink:0';
|
||||
const cohHeader = document.createElement('div');
|
||||
cohHeader.style.cssText = 'padding:10px 14px;font-size:11px;font-weight:500;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.6px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center';
|
||||
cohHeader.innerHTML = '<span>Coherence Evolution</span><span style="font-size:10px;color:var(--text-muted);font-family:var(--font-mono)">Dashed line = 0.90 threshold</span>';
|
||||
cohPanel.appendChild(cohHeader);
|
||||
this.coherenceCanvas = document.createElement('canvas');
|
||||
this.coherenceCanvas.style.cssText = 'width:100%;height:140px;display:block';
|
||||
cohPanel.appendChild(this.coherenceCanvas);
|
||||
content.appendChild(cohPanel);
|
||||
|
||||
// Witness log
|
||||
const logPanel = document.createElement('div');
|
||||
logPanel.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;flex:1;min-height:200px;display:flex;flex-direction:column';
|
||||
const logHeader = document.createElement('div');
|
||||
logHeader.style.cssText = 'padding:10px 14px;font-size:11px;font-weight:500;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.6px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;flex-shrink:0';
|
||||
logHeader.innerHTML = '<span>Witness Log</span><span style="font-size:10px;color:var(--text-muted);font-family:var(--font-mono)">Hash-linked entries</span>';
|
||||
logPanel.appendChild(logHeader);
|
||||
// Column headers
|
||||
const colHeaders = document.createElement('div');
|
||||
colHeaders.style.cssText = 'display:flex;align-items:center;gap:10px;padding:6px 14px;border-bottom:1px solid var(--border);font-size:10px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.4px;flex-shrink:0';
|
||||
colHeaders.innerHTML = `
|
||||
<span style="min-width:60px">Time</span>
|
||||
<span style="min-width:52px">Type</span>
|
||||
<span style="min-width:90px">Witness</span>
|
||||
<span style="flex:1">Action</span>
|
||||
<span style="min-width:50px;text-align:right">Coh.</span>
|
||||
<span style="min-width:100px;text-align:right">Hash</span>
|
||||
`;
|
||||
logPanel.appendChild(colHeaders);
|
||||
this.logEl = document.createElement('div');
|
||||
this.logEl.style.cssText = 'flex:1;overflow-y:auto;font-family:var(--font-mono);font-size:11px';
|
||||
logPanel.appendChild(this.logEl);
|
||||
content.appendChild(logPanel);
|
||||
|
||||
this.loadData();
|
||||
|
||||
this.unsubWs = onEvent((ev: LiveEvent) => {
|
||||
if (ev.event_type === 'witness') {
|
||||
this.addLiveEntry(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async loadData(): Promise<void> {
|
||||
let log: WitnessLogResponse;
|
||||
try {
|
||||
log = await fetchWitnessLog();
|
||||
} catch {
|
||||
log = {
|
||||
entries: [], chain_length: 0, integrity: '--', hash_algorithm: 'SHAKE-256',
|
||||
root_hash: '--', genesis_hash: '--', mean_coherence: 0, min_coherence: 0, total_epochs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
this.chainMeta = {
|
||||
integrity: log.integrity,
|
||||
hashAlgo: log.hash_algorithm,
|
||||
rootHash: log.root_hash,
|
||||
meanCoherence: log.mean_coherence,
|
||||
minCoherence: log.min_coherence,
|
||||
};
|
||||
|
||||
this.entries = log.entries.map((e: ApiWitnessEntry) => ({
|
||||
timestamp: e.timestamp.includes('T') ? e.timestamp.split('T')[1]?.substring(0, 8) ?? '' : e.timestamp,
|
||||
type: e.type,
|
||||
witness: e.witness,
|
||||
action: e.action,
|
||||
hash: e.hash,
|
||||
prevHash: e.prev_hash,
|
||||
coherence: e.coherence,
|
||||
measurement: e.measurement,
|
||||
epoch: e.epoch,
|
||||
}));
|
||||
|
||||
if (this.entries.length === 0) {
|
||||
this.entries = this.generateDemoEntries();
|
||||
}
|
||||
|
||||
this.updateMetrics(log);
|
||||
this.renderChain();
|
||||
this.renderCoherence();
|
||||
this.renderLog();
|
||||
}
|
||||
|
||||
private updateMetrics(log: WitnessLogResponse): void {
|
||||
const set = (key: string, val: string) => {
|
||||
const el = this.metricsEls[key];
|
||||
if (el) el.textContent = val;
|
||||
};
|
||||
set('entries', String(this.entries.length));
|
||||
set('integrity', this.chainMeta.integrity);
|
||||
set('coherence', this.chainMeta.meanCoherence > 0 ? this.chainMeta.meanCoherence.toFixed(4) : '--');
|
||||
set('minCoherence', this.chainMeta.minCoherence > 0 ? this.chainMeta.minCoherence.toFixed(4) : '--');
|
||||
set('depth', String(log.total_epochs));
|
||||
set('rootHash', this.chainMeta.rootHash.substring(0, 12) + '...');
|
||||
|
||||
// Color the min coherence if below threshold
|
||||
const minEl = this.metricsEls['minCoherence'];
|
||||
if (minEl && this.chainMeta.minCoherence > 0 && this.chainMeta.minCoherence < 0.9) {
|
||||
minEl.style.color = '#FFB020';
|
||||
}
|
||||
// Color integrity
|
||||
const intEl = this.metricsEls['integrity'];
|
||||
if (intEl) {
|
||||
intEl.style.color = this.chainMeta.integrity === 'VALID' ? '#2ECC71' : '#FF4D4D';
|
||||
}
|
||||
}
|
||||
|
||||
private renderChain(): void {
|
||||
const canvas = this.chainCanvas;
|
||||
if (!canvas) return;
|
||||
|
||||
const rect = canvas.parentElement?.getBoundingClientRect();
|
||||
const w = rect?.width ?? 800;
|
||||
const h = 120;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
canvas.style.width = `${w}px`;
|
||||
canvas.style.height = `${h}px`;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const n = this.entries.length;
|
||||
if (n === 0) return;
|
||||
|
||||
const padX = 40;
|
||||
const padY = 20;
|
||||
const innerW = w - padX * 2;
|
||||
const midY = h / 2;
|
||||
const nodeR = 8;
|
||||
|
||||
// Draw connecting lines first
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
const x1 = padX + (i / (n - 1)) * innerW;
|
||||
const x2 = padX + ((i + 1) / (n - 1)) * innerW;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1 + nodeR, midY);
|
||||
ctx.lineTo(x2 - nodeR, midY);
|
||||
ctx.strokeStyle = '#1E2630';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Arrow head
|
||||
const ax = x2 - nodeR - 6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(ax, midY - 3);
|
||||
ctx.lineTo(ax + 6, midY);
|
||||
ctx.lineTo(ax, midY + 3);
|
||||
ctx.fillStyle = '#1E2630';
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Draw nodes
|
||||
for (let i = 0; i < n; i++) {
|
||||
const entry = this.entries[i];
|
||||
const x = padX + (n > 1 ? (i / (n - 1)) * innerW : innerW / 2);
|
||||
const color = TYPE_COLORS[entry.type] ?? '#00E5FF';
|
||||
const isSelected = i === this.selectedIdx;
|
||||
|
||||
// Glow for selected
|
||||
if (isSelected) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, midY, nodeR + 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color.replace(')', ', 0.15)').replace('rgb', 'rgba').replace('#', '');
|
||||
// Use a simpler glow approach
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = 12;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
// Outer ring
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, midY, nodeR, 0, Math.PI * 2);
|
||||
ctx.fillStyle = isSelected ? color : 'transparent';
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// Inner dot
|
||||
if (!isSelected) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, midY, 3, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Witness label (above)
|
||||
ctx.fillStyle = '#8B949E';
|
||||
ctx.font = '9px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
const label = entry.witness.replace('W_', '');
|
||||
ctx.fillText(label, x, midY - nodeR - padY + 8);
|
||||
|
||||
// Coherence label (below)
|
||||
ctx.fillStyle = entry.coherence < 0.9 ? '#FFB020' : '#484F58';
|
||||
ctx.font = '9px monospace';
|
||||
ctx.fillText(entry.coherence.toFixed(2), x, midY + nodeR + 14);
|
||||
|
||||
// Hash snippet (below coherence)
|
||||
ctx.fillStyle = '#30363D';
|
||||
ctx.font = '8px monospace';
|
||||
ctx.fillText(entry.hash.substring(0, 6), x, midY + nodeR + 24);
|
||||
}
|
||||
}
|
||||
|
||||
private onChainClick(e: MouseEvent): void {
|
||||
const canvas = this.chainCanvas;
|
||||
if (!canvas || this.entries.length === 0) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const n = this.entries.length;
|
||||
const padX = 40;
|
||||
const innerW = rect.width - padX * 2;
|
||||
|
||||
let closest = -1;
|
||||
let minDist = Infinity;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const nx = padX + (n > 1 ? (i / (n - 1)) * innerW : innerW / 2);
|
||||
const dist = Math.abs(x - nx);
|
||||
if (dist < minDist && dist < 20) {
|
||||
minDist = dist;
|
||||
closest = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (closest >= 0) {
|
||||
this.selectedIdx = closest === this.selectedIdx ? -1 : closest;
|
||||
this.renderChain();
|
||||
this.showDetail(this.selectedIdx >= 0 ? this.entries[this.selectedIdx] : null);
|
||||
}
|
||||
}
|
||||
|
||||
private showDetail(entry: WitnessEntry | null): void {
|
||||
if (!this.detailEl) return;
|
||||
if (!entry) {
|
||||
this.detailEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const color = TYPE_COLORS[entry.type] ?? '#00E5FF';
|
||||
const typeDesc = TYPE_LABELS[entry.type] ?? '';
|
||||
const cohColor = entry.coherence < 0.9 ? '#FFB020' : '#2ECC71';
|
||||
|
||||
this.detailEl.style.display = 'block';
|
||||
this.detailEl.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
||||
<div style="width:10px;height:10px;border-radius:50%;background:${color}"></div>
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text-primary);font-family:var(--font-mono)">${entry.witness}</span>
|
||||
<span style="font-size:10px;padding:2px 8px;border-radius:3px;background:${color}22;color:${color};font-weight:600;text-transform:uppercase">${entry.type}</span>
|
||||
<span style="font-size:10px;color:var(--text-muted)">${typeDesc}</span>
|
||||
<span style="margin-left:auto;font-size:11px;color:var(--text-muted);font-family:var(--font-mono)">Epoch ${entry.epoch}</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div>
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px">Action</div>
|
||||
<div style="font-size:12px;color:var(--text-primary);line-height:1.5">${entry.action}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px">Measurement</div>
|
||||
<div style="font-size:12px;color:var(--accent);font-family:var(--font-mono)">${entry.measurement ?? 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-top:12px;padding-top:12px;border-top:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:2px">Coherence</div>
|
||||
<div style="font-size:16px;font-weight:500;color:${cohColor};font-family:var(--font-mono)">${entry.coherence.toFixed(4)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:2px">Hash</div>
|
||||
<div style="font-size:11px;color:var(--text-primary);font-family:var(--font-mono)">${entry.hash}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:2px">Previous Hash</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);font-family:var(--font-mono)">${entry.prevHash}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCoherence(): void {
|
||||
const canvas = this.coherenceCanvas;
|
||||
if (!canvas) return;
|
||||
|
||||
const rect = canvas.parentElement?.getBoundingClientRect();
|
||||
const w = rect?.width ?? 800;
|
||||
const h = 140;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
canvas.style.width = `${w}px`;
|
||||
canvas.style.height = `${h}px`;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const n = this.entries.length;
|
||||
if (n === 0) return;
|
||||
|
||||
const padL = 50, padR = 20, padT = 16, padB = 28;
|
||||
const iW = w - padL - padR;
|
||||
const iH = h - padT - padB;
|
||||
|
||||
// Y axis: 0.80 to 1.00
|
||||
const yMin = 0.80, yMax = 1.01;
|
||||
const toX = (i: number) => padL + (n > 1 ? (i / (n - 1)) * iW : iW / 2);
|
||||
const toY = (v: number) => padT + (1 - (v - yMin) / (yMax - yMin)) * iH;
|
||||
|
||||
// Grid lines
|
||||
ctx.strokeStyle = '#161C24';
|
||||
ctx.lineWidth = 1;
|
||||
for (let v = 0.80; v <= 1.001; v += 0.05) {
|
||||
const y = toY(v);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padL, y);
|
||||
ctx.lineTo(w - padR, y);
|
||||
ctx.stroke();
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = '#484F58';
|
||||
ctx.font = '10px monospace';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(v.toFixed(2), padL - 6, y + 4);
|
||||
}
|
||||
|
||||
// Threshold line at 0.90
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.strokeStyle = '#FFB02066';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padL, toY(0.90));
|
||||
ctx.lineTo(w - padR, toY(0.90));
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Label threshold
|
||||
ctx.fillStyle = '#FFB020';
|
||||
ctx.font = '9px monospace';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText('threshold', w - padR - 55, toY(0.90) - 4);
|
||||
|
||||
// Fill area under coherence line
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(toX(0), toY(yMin));
|
||||
for (let i = 0; i < n; i++) {
|
||||
ctx.lineTo(toX(i), toY(Math.max(yMin, this.entries[i].coherence)));
|
||||
}
|
||||
ctx.lineTo(toX(n - 1), toY(yMin));
|
||||
ctx.closePath();
|
||||
const grad = ctx.createLinearGradient(0, padT, 0, padT + iH);
|
||||
grad.addColorStop(0, 'rgba(0, 229, 255, 0.08)');
|
||||
grad.addColorStop(1, 'rgba(0, 229, 255, 0.01)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
|
||||
// Coherence line
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const x = toX(i);
|
||||
const y = toY(Math.max(yMin, this.entries[i].coherence));
|
||||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.strokeStyle = '#00E5FF';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Data points
|
||||
for (let i = 0; i < n; i++) {
|
||||
const x = toX(i);
|
||||
const coh = Math.max(yMin, this.entries[i].coherence);
|
||||
const y = toY(coh);
|
||||
const color = this.entries[i].coherence < 0.9
|
||||
? '#FFB020'
|
||||
: TYPE_COLORS[this.entries[i].type] ?? '#00E5FF';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#0B0F14';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
// X-axis labels
|
||||
ctx.fillStyle = '#484F58';
|
||||
ctx.font = '8px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
const label = this.entries[i].witness.replace('W_', '');
|
||||
if (n <= 20 || i % 2 === 0) {
|
||||
ctx.fillText(label, x, h - padB + 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderLog(): void {
|
||||
if (!this.logEl) return;
|
||||
this.logEl.innerHTML = '';
|
||||
|
||||
for (let i = 0; i < this.entries.length; i++) {
|
||||
this.appendLogEntry(this.entries[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
private appendLogEntry(entry: WitnessEntry, idx: number): void {
|
||||
if (!this.logEl) return;
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;align-items:center;gap:10px;padding:6px 14px;border-bottom:1px solid var(--border-subtle);cursor:pointer;transition:background 0.1s';
|
||||
row.addEventListener('mouseenter', () => { row.style.background = 'rgba(255,255,255,0.015)'; });
|
||||
row.addEventListener('mouseleave', () => { row.style.background = idx === this.selectedIdx ? 'rgba(0,229,255,0.04)' : ''; });
|
||||
row.addEventListener('click', () => {
|
||||
this.selectedIdx = idx === this.selectedIdx ? -1 : idx;
|
||||
this.renderChain();
|
||||
this.showDetail(this.selectedIdx >= 0 ? this.entries[this.selectedIdx] : null);
|
||||
// Highlight the row
|
||||
const rows = this.logEl?.children;
|
||||
if (rows) {
|
||||
for (let r = 0; r < rows.length; r++) {
|
||||
(rows[r] as HTMLElement).style.background = r === this.selectedIdx ? 'rgba(0,229,255,0.04)' : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const color = TYPE_COLORS[entry.type] ?? '#00E5FF';
|
||||
const cohColor = entry.coherence < 0.9 ? '#FFB020' : '#484F58';
|
||||
|
||||
row.innerHTML = `
|
||||
<span style="color:var(--text-muted);min-width:60px;white-space:nowrap;font-size:10px">${entry.timestamp}</span>
|
||||
<span style="padding:2px 8px;border-radius:3px;font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:0.3px;min-width:52px;text-align:center;background:${color}18;color:${color}">${entry.type}</span>
|
||||
<span style="color:var(--accent);min-width:90px;font-size:11px">${entry.witness}</span>
|
||||
<span style="color:var(--text-primary);flex:1;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${entry.action}">${entry.action}</span>
|
||||
<span style="color:${cohColor};min-width:50px;text-align:right;font-size:11px">${entry.coherence.toFixed(2)}</span>
|
||||
<span style="color:var(--text-muted);font-size:10px;min-width:100px;text-align:right" title="Hash: ${entry.hash} | Prev: ${entry.prevHash}">${entry.hash.substring(0, 8)}..${entry.prevHash.substring(0, 4)}</span>
|
||||
`;
|
||||
this.logEl.appendChild(row);
|
||||
}
|
||||
|
||||
private addLiveEntry(ev: LiveEvent): void {
|
||||
const entry: WitnessEntry = {
|
||||
timestamp: new Date(ev.timestamp * 1000).toISOString().substring(11, 19),
|
||||
type: String(ev.data['type'] ?? 'commit'),
|
||||
witness: String(ev.data['witness'] ?? 'W_live'),
|
||||
action: String(ev.data['action'] ?? 'live_event'),
|
||||
hash: String(ev.data['hash'] ?? this.fakeHash('live')),
|
||||
prevHash: this.entries.length > 0 ? this.entries[this.entries.length - 1].hash : '0000000000000000',
|
||||
coherence: Number(ev.data['coherence'] ?? 1.0),
|
||||
measurement: ev.data['measurement'] ? String(ev.data['measurement']) : null,
|
||||
epoch: this.entries.length,
|
||||
};
|
||||
this.entries.push(entry);
|
||||
this.appendLogEntry(entry, this.entries.length - 1);
|
||||
this.renderChain();
|
||||
this.renderCoherence();
|
||||
|
||||
// Update entry count metric
|
||||
const el = this.metricsEls['entries'];
|
||||
if (el) el.textContent = String(this.entries.length);
|
||||
|
||||
// Auto-scroll log
|
||||
if (this.logEl) {
|
||||
this.logEl.scrollTop = this.logEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private fakeHash(seed: string): string {
|
||||
let h = 0;
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
h = ((h << 5) - h + seed.charCodeAt(i)) | 0;
|
||||
}
|
||||
return Math.abs(h).toString(16).padStart(16, '0').substring(0, 16);
|
||||
}
|
||||
|
||||
private generateDemoEntries(): WitnessEntry[] {
|
||||
const witnesses = [
|
||||
{ w: 'W_root', t: 'seal', a: 'Chain initialized — genesis anchor', m: null },
|
||||
{ w: 'W_photometry', t: 'commit', a: 'Kepler light curves ingested (196K targets)', m: 'transit_depth_rms=4.2e-5' },
|
||||
{ w: 'W_periodogram', t: 'commit', a: 'BLS search completed — 2,842 signals', m: 'bls_power_max=42.7' },
|
||||
{ w: 'W_stellar', t: 'commit', a: 'Stellar parameters derived (Gaia DR3)', m: 'T_eff_sigma=47K' },
|
||||
{ w: 'W_transit', t: 'merge', a: 'Transit model merged with stellar params', m: 'R_p_range=0.92-2.61' },
|
||||
{ w: 'W_radial_velocity', t: 'commit', a: 'HARPS RV data — mass constraints', m: 'K_rv_range=0.089-3.2' },
|
||||
{ w: 'W_orbit', t: 'commit', a: 'Orbital solutions — HZ classification', m: 'hz_candidates=10' },
|
||||
{ w: 'W_esi', t: 'commit', a: 'ESI ranking computed', m: 'esi_top=0.93' },
|
||||
{ w: 'W_spectroscopy', t: 'merge', a: 'JWST atmospheric observations merged', m: 'CH4+CO2_detected' },
|
||||
{ w: 'W_biosig', t: 'commit', a: 'Biosignature scoring pipeline', m: 'diseq_max=0.82' },
|
||||
{ w: 'W_blind', t: 'commit', a: 'Blind test passed (τ=1.0)', m: 'kendall_tau=1.000' },
|
||||
{ w: 'W_seal', t: 'verify', a: 'Chain sealed — Ed25519 signed', m: 'chain_length=12' },
|
||||
];
|
||||
let prevHash = '0000000000000000';
|
||||
return witnesses.map((w, i) => {
|
||||
const hash = this.fakeHash(w.w + i);
|
||||
const entry: WitnessEntry = {
|
||||
timestamp: new Date(Date.now() - (witnesses.length - i) * 120000).toISOString().substring(11, 19),
|
||||
type: w.t,
|
||||
witness: w.w,
|
||||
action: w.a,
|
||||
hash,
|
||||
prevHash,
|
||||
coherence: 1.0 - i * 0.01,
|
||||
measurement: w.m,
|
||||
epoch: i,
|
||||
};
|
||||
prevHash = hash;
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
this.unsubWs?.();
|
||||
this.logEl = null;
|
||||
this.chainCanvas = null;
|
||||
this.coherenceCanvas = null;
|
||||
this.detailEl = null;
|
||||
this.metricsEls = {};
|
||||
this.container = null;
|
||||
this.entries = [];
|
||||
this.selectedIdx = -1;
|
||||
}
|
||||
}
|
||||
96
vendor/ruvector/examples/rvf/dashboard/src/ws.ts
vendored
Normal file
96
vendor/ruvector/examples/rvf/dashboard/src/ws.ts
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
export interface LiveEvent {
|
||||
event_type: string;
|
||||
timestamp: number;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type EventCallback = (event: LiveEvent) => void;
|
||||
|
||||
const listeners: EventCallback[] = [];
|
||||
let socket: WebSocket | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let reconnectDelay = 1000;
|
||||
let intentionalClose = false;
|
||||
|
||||
const MAX_RECONNECT_DELAY = 30000;
|
||||
const RECONNECT_BACKOFF = 2;
|
||||
|
||||
function handleMessage(raw: MessageEvent): void {
|
||||
try {
|
||||
const event = JSON.parse(raw.data as string) as LiveEvent;
|
||||
for (const cb of listeners) {
|
||||
cb(event);
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed messages
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleReconnect(): void {
|
||||
if (intentionalClose) return;
|
||||
if (reconnectTimer) return;
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
openSocket();
|
||||
}, reconnectDelay);
|
||||
|
||||
reconnectDelay = Math.min(reconnectDelay * RECONNECT_BACKOFF, MAX_RECONNECT_DELAY);
|
||||
}
|
||||
|
||||
function openSocket(): void {
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const url = `${protocol}//${location.host}/ws/live`;
|
||||
|
||||
try {
|
||||
socket = new WebSocket(url);
|
||||
} catch {
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
reconnectDelay = 1000;
|
||||
});
|
||||
|
||||
socket.addEventListener('message', handleMessage);
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
socket = null;
|
||||
scheduleReconnect();
|
||||
});
|
||||
|
||||
socket.addEventListener('error', () => {
|
||||
socket?.close();
|
||||
});
|
||||
}
|
||||
|
||||
export function onEvent(callback: EventCallback): () => void {
|
||||
listeners.push(callback);
|
||||
return () => {
|
||||
const idx = listeners.indexOf(callback);
|
||||
if (idx >= 0) listeners.splice(idx, 1);
|
||||
};
|
||||
}
|
||||
|
||||
export function connect(): void {
|
||||
intentionalClose = false;
|
||||
reconnectDelay = 1000;
|
||||
openSocket();
|
||||
}
|
||||
|
||||
export function disconnect(): void {
|
||||
intentionalClose = true;
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
17
vendor/ruvector/examples/rvf/dashboard/tsconfig.json
vendored
Normal file
17
vendor/ruvector/examples/rvf/dashboard/tsconfig.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
17
vendor/ruvector/examples/rvf/dashboard/vite.config.ts
vendored
Normal file
17
vendor/ruvector/examples/rvf/dashboard/vite.config.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
base: '/',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
three: ['three'],
|
||||
d3: ['d3-scale', 'd3-axis', 'd3-shape', 'd3-selection'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user