Spaces:
Running
Running
deploy(hf): sync szl-holdings/a11oy@main derived COPY set
Browse filesReusable Dockerfile-COPY-derived deploy from szl-holdings/a11oy main.
Files: 989 Pruned: 0
Derived from Dockerfile COPY sources (NO hand-maintained allowlist).
Signed-off-by: SZL Holdings <noreply@szlholdings.ai>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- static/3d/holo-lab.html +279 -79
static/3d/holo-lab.html
CHANGED
|
@@ -29,16 +29,54 @@
|
|
| 29 |
border:1px solid rgba(57,230,212,.35); color:#bfeee7; background:rgba(57,230,212,.06);
|
| 30 |
}
|
| 31 |
.badge.warn { border-color:rgba(240,200,90,.45); color:#f0d98a; background:rgba(240,200,90,.06); }
|
|
|
|
| 32 |
.status {
|
| 33 |
position:fixed; left:152px; bottom:20px; z-index:5; font-size:11px; letter-spacing:.10em;
|
| 34 |
color:#6fa6a0; pointer-events:none;
|
| 35 |
}
|
| 36 |
.status b { color:#9fe6dd; font-weight:600; }
|
| 37 |
.legend {
|
| 38 |
-
position:fixed; right:22px; bottom:
|
| 39 |
color:#6fa6a0; text-align:right; pointer-events:none; line-height:1.7;
|
| 40 |
}
|
| 41 |
.dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-left:8px; vertical-align:middle; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
</style>
|
| 43 |
</head>
|
| 44 |
<body>
|
|
@@ -46,20 +84,34 @@
|
|
| 46 |
<canvas id="gl"></canvas>
|
| 47 |
<canvas id="fb" hidden></canvas>
|
| 48 |
</div>
|
|
|
|
| 49 |
|
| 50 |
<div class="hud">
|
| 51 |
<h1 class="title">HOLOGRAPHIC ESTATE</h1>
|
| 52 |
-
<p class="sub">Point-field estate
|
| 53 |
<div class="badges">
|
| 54 |
-
<span class="badge warn">SAMPLE — procedural
|
| 55 |
-
<span class="badge">
|
| 56 |
</div>
|
| 57 |
</div>
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
<div class="legend">
|
| 60 |
-
structure<span class="dot" style="background:#eafffb"></span><br/>
|
| 61 |
roof line<span class="dot" style="background:#39e6d4"></span><br/>
|
| 62 |
-
grounds<span class="dot" style="background:#2f7fb0"></span>
|
|
|
|
|
|
|
| 63 |
</div>
|
| 64 |
|
| 65 |
<div class="status">renderer: <b id="rstat">initialising…</b> · <span id="rnote">building point field</span></div>
|
|
@@ -74,15 +126,24 @@
|
|
| 74 |
</script>
|
| 75 |
|
| 76 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
// ---- Shared procedural estate point field (single source for WebGL + 2D fallback) ----
|
| 78 |
-
// Honest: this is a generated model of a house + grounds, NOT a photogrammetric capture.
|
| 79 |
function buildEstatePoints() {
|
| 80 |
const pts = []; // {x,y,z, c:[r,g,b]}
|
| 81 |
const push = (x,y,z,c) => pts.push({x,y,z,c});
|
| 82 |
const CYAN = [0.22,0.90,0.83], WHITE = [0.92,1.0,0.98], BLUE = [0.18,0.50,0.69];
|
| 83 |
const jit = (n) => (Math.random()-0.5)*n;
|
| 84 |
|
| 85 |
-
// House body: box centred at origin, width 6 (x), height 3 (y), depth 4 (z), base at y=0
|
| 86 |
const W=6, H=3, D=4;
|
| 87 |
const wallDensity = 2600;
|
| 88 |
for (let i=0;i<wallDensity;i++){
|
|
@@ -95,36 +156,28 @@
|
|
| 95 |
else { z=D/2; x=(Math.random()-0.5)*W; }
|
| 96 |
push(x+jit(0.03), y+jit(0.03), z+jit(0.03), WHITE);
|
| 97 |
}
|
| 98 |
-
// Roof: two sloped planes meeting at a ridge (y from H to H+1.8)
|
| 99 |
const roofDensity = 1700, RH=1.8;
|
| 100 |
for (let i=0;i<roofDensity;i++){
|
| 101 |
-
const t = Math.random();
|
| 102 |
-
const along = (Math.random()-0.5)*D;
|
| 103 |
const side = Math.random()<0.5 ? -1 : 1;
|
| 104 |
const x = side * (W/2) * (1-t);
|
| 105 |
const y = H + RH * t;
|
| 106 |
push(x+jit(0.02), y+jit(0.02), along+jit(0.03), CYAN);
|
| 107 |
}
|
| 108 |
-
// Ridge line accent
|
| 109 |
for (let i=0;i<260;i++){ push(jit(0.05), H+RH+jit(0.02), (Math.random()-0.5)*D, CYAN); }
|
| 110 |
-
// Windows: brighter clusters on front face (z = D/2)
|
| 111 |
const winCenters = [[-1.6,1.3],[1.6,1.3],[0,0.9]];
|
| 112 |
for (const [wx,wy] of winCenters){
|
| 113 |
-
for (let i=0;i<230;i++){
|
| 114 |
-
push(wx+jit(0.5), wy+jit(0.6), D/2+0.02, WHITE);
|
| 115 |
-
}
|
| 116 |
}
|
| 117 |
-
// Grounds: radial point grid on y=0 plane, fading outward
|
| 118 |
const groundR = 11;
|
| 119 |
for (let i=0;i<3200;i++){
|
| 120 |
const r = Math.pow(Math.random(),0.6)*groundR;
|
| 121 |
const a = Math.random()*Math.PI*2;
|
| 122 |
const x = Math.cos(a)*r, z = Math.sin(a)*r;
|
| 123 |
-
// skip the footprint interior so the house reads clearly
|
| 124 |
if (Math.abs(x)<W/2+0.3 && Math.abs(z)<D/2+0.3) continue;
|
| 125 |
push(x, jit(0.04), z, BLUE);
|
| 126 |
}
|
| 127 |
-
// A few tall accents (trees) at corners of the plot
|
| 128 |
const trees = [[-7,-6],[7,-6.5],[-6.5,6],[7.5,6]];
|
| 129 |
for (const [tx,tz] of trees){
|
| 130 |
for (let i=0;i<420;i++){
|
|
@@ -139,14 +192,136 @@
|
|
| 139 |
const ESTATE = buildEstatePoints();
|
| 140 |
const rstat = document.getElementById('rstat');
|
| 141 |
const rnote = document.getElementById('rnote');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
function run2DFallback(reason){
|
| 145 |
const glc = document.getElementById('gl'); glc.hidden = true;
|
| 146 |
const c = document.getElementById('fb'); c.hidden = false;
|
| 147 |
const ctx = c.getContext('2d');
|
| 148 |
-
rstat.textContent = '2D fallback';
|
| 149 |
-
rstat.style.color = '#f0d98a';
|
| 150 |
rnote.textContent = reason || 'WebGL2 unavailable — projecting same model';
|
| 151 |
let W,H,DPR;
|
| 152 |
function resize(){
|
|
@@ -155,38 +330,42 @@
|
|
| 155 |
c.width = W*DPR; c.height = H*DPR; ctx.setTransform(DPR,0,0,DPR,0,0);
|
| 156 |
}
|
| 157 |
window.addEventListener('resize', resize); resize();
|
| 158 |
-
let ang = 0;
|
| 159 |
-
|
| 160 |
-
function frame(){
|
| 161 |
-
ang += 0.0032;
|
| 162 |
-
ctx.clearRect(0,0,W,H);
|
| 163 |
-
const cx=W/2, cy=H/2 + 40;
|
| 164 |
const ca=Math.cos(ang), sa=Math.sin(ang);
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
const proj = [];
|
| 167 |
-
for (const p of ESTATE){
|
| 168 |
-
|
| 169 |
-
const z = p.x*sa + p.z*ca + camDist;
|
| 170 |
-
const y = p.y - camY;
|
| 171 |
-
if (z<=0.1) continue;
|
| 172 |
-
const s = focal/z;
|
| 173 |
-
proj.push({ sx: cx + x*s, sy: cy - y*s, z, s, c:p.c });
|
| 174 |
-
}
|
| 175 |
proj.sort((a,b)=>b.z-a.z);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
for (const q of proj){
|
| 177 |
-
const
|
| 178 |
-
const
|
|
|
|
| 179 |
ctx.beginPath();
|
| 180 |
ctx.fillStyle = `rgba(${(q.c[0]*255)|0},${(q.c[1]*255)|0},${(q.c[2]*255)|0},${a})`;
|
| 181 |
-
ctx.arc(q.sx, q.sy, r, 0, Math.PI*2);
|
| 182 |
-
ctx.fill();
|
| 183 |
}
|
|
|
|
| 184 |
requestAnimationFrame(frame);
|
| 185 |
}
|
| 186 |
frame();
|
| 187 |
}
|
| 188 |
|
| 189 |
-
// ---- WebGL2 path (soft gaussian
|
| 190 |
async function runWebGL(){
|
| 191 |
const canvas = document.getElementById('gl');
|
| 192 |
const gl2 = canvas.getContext('webgl2');
|
|
@@ -207,51 +386,70 @@
|
|
| 207 |
camera.position.set(13, 8, 15);
|
| 208 |
|
| 209 |
const controls = new OrbitControls(camera, renderer.domElement);
|
| 210 |
-
controls.enableDamping = true; controls.autoRotate = true; controls.autoRotateSpeed = 0.
|
| 211 |
-
controls.target.set(0,
|
| 212 |
|
|
|
|
| 213 |
const n = ESTATE.length;
|
| 214 |
-
const positions = new Float32Array(n*3);
|
| 215 |
-
|
| 216 |
-
for (let i=0;i<n;i++){
|
| 217 |
-
const p = ESTATE[i];
|
| 218 |
positions[i*3]=p.x; positions[i*3+1]=p.y; positions[i*3+2]=p.z;
|
| 219 |
-
colors[i*3]=p.c[0]; colors[i*3+1]=p.c[1]; colors[i*3+2]=p.c[2];
|
| 220 |
-
}
|
| 221 |
const geo = new THREE.BufferGeometry();
|
| 222 |
geo.setAttribute('position', new THREE.BufferAttribute(positions,3));
|
| 223 |
geo.setAttribute('color', new THREE.BufferAttribute(colors,3));
|
| 224 |
|
| 225 |
-
const
|
| 226 |
-
uniforms:
|
| 227 |
-
transparent:true, depthWrite:false, blending:THREE.AdditiveBlending,
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
void main(){
|
| 233 |
-
|
| 234 |
-
vec4 mv = modelViewMatrix * vec4(position,1.0);
|
| 235 |
-
gl_PointSize = uSize * uPixelRatio * (1.0 / -mv.z) * 4.0;
|
| 236 |
-
gl_Position = projectionMatrix * mv;
|
| 237 |
-
}`,
|
| 238 |
-
fragmentShader:`
|
| 239 |
-
varying vec3 vColor;
|
| 240 |
-
void main(){
|
| 241 |
-
vec2 d = gl_PointCoord - vec2(0.5);
|
| 242 |
-
float r = dot(d,d);
|
| 243 |
-
float a = exp(-r*7.0) * 0.30; // soft gaussian, low per-point alpha
|
| 244 |
-
if (a < 0.01) discard;
|
| 245 |
-
gl_FragColor = vec4(vColor, a);
|
| 246 |
-
}`
|
| 247 |
});
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
const composer = new EffectComposer(renderer);
|
| 251 |
composer.addPass(new RenderPass(scene, camera));
|
| 252 |
-
|
| 253 |
-
new THREE.Vector2(window.innerWidth, window.innerHeight), 0.7, 0.6, 0.25);
|
| 254 |
-
composer.addPass(bloom);
|
| 255 |
|
| 256 |
window.addEventListener('resize', ()=>{
|
| 257 |
camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix();
|
|
@@ -260,15 +458,17 @@
|
|
| 260 |
});
|
| 261 |
|
| 262 |
rstat.textContent = 'WebGL2'; rstat.style.color = '#9fe6dd';
|
| 263 |
-
rnote.textContent = `three r${THREE.REVISION} ·
|
| 264 |
|
| 265 |
-
(
|
|
|
|
| 266 |
}
|
| 267 |
|
| 268 |
-
runWebGL().catch(err => {
|
| 269 |
run2DFallback(err && err.message === 'no-webgl2'
|
| 270 |
? 'WebGL2 unavailable — projecting same model'
|
| 271 |
: 'renderer offline — projecting same model');
|
|
|
|
| 272 |
});
|
| 273 |
</script>
|
| 274 |
</body>
|
|
|
|
| 29 |
border:1px solid rgba(57,230,212,.35); color:#bfeee7; background:rgba(57,230,212,.06);
|
| 30 |
}
|
| 31 |
.badge.warn { border-color:rgba(240,200,90,.45); color:#f0d98a; background:rgba(240,200,90,.06); }
|
| 32 |
+
.badge.live { border-color:rgba(90,240,170,.5); color:#8ff0c4; background:rgba(90,240,170,.07); }
|
| 33 |
.status {
|
| 34 |
position:fixed; left:152px; bottom:20px; z-index:5; font-size:11px; letter-spacing:.10em;
|
| 35 |
color:#6fa6a0; pointer-events:none;
|
| 36 |
}
|
| 37 |
.status b { color:#9fe6dd; font-weight:600; }
|
| 38 |
.legend {
|
| 39 |
+
position:fixed; right:22px; bottom:18px; z-index:5; font-size:10.5px; letter-spacing:.06em;
|
| 40 |
color:#6fa6a0; text-align:right; pointer-events:none; line-height:1.7;
|
| 41 |
}
|
| 42 |
.dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-left:8px; vertical-align:middle; }
|
| 43 |
+
|
| 44 |
+
/* ---- Estate-brain query console ---- */
|
| 45 |
+
#brain {
|
| 46 |
+
position:fixed; right:22px; top:78px; z-index:6; width:min(340px,86vw);
|
| 47 |
+
background:rgba(6,12,18,.82); border:1px solid rgba(57,230,212,.28); border-radius:14px;
|
| 48 |
+
backdrop-filter:blur(7px); padding:14px 14px 12px; pointer-events:auto;
|
| 49 |
+
box-shadow:0 18px 50px rgba(0,0,0,.45);
|
| 50 |
+
}
|
| 51 |
+
#brain h2 { margin:0 0 2px; font-size:12px; letter-spacing:.20em; color:#eafffb; font-weight:600; }
|
| 52 |
+
#brain .hint { margin:0 0 10px; font-size:10px; letter-spacing:.05em; color:#6fa6a0; line-height:1.55; }
|
| 53 |
+
#brain .row { display:flex; gap:7px; }
|
| 54 |
+
#q {
|
| 55 |
+
flex:1; min-width:0; background:rgba(3,7,11,.9); border:1px solid rgba(57,230,212,.30);
|
| 56 |
+
color:#dffaf5; border-radius:9px; padding:9px 10px; font:inherit; font-size:12px; outline:none;
|
| 57 |
+
}
|
| 58 |
+
#q:focus { border-color:rgba(57,230,212,.7); }
|
| 59 |
+
#ask {
|
| 60 |
+
border:1px solid rgba(57,230,212,.45); background:rgba(57,230,212,.12); color:#bff5ec;
|
| 61 |
+
border-radius:9px; padding:0 13px; font:inherit; font-size:11px; letter-spacing:.08em;
|
| 62 |
+
cursor:pointer; white-space:nowrap;
|
| 63 |
+
}
|
| 64 |
+
#ask:hover { background:rgba(57,230,212,.22); }
|
| 65 |
+
#ask:disabled { opacity:.5; cursor:default; }
|
| 66 |
+
#ans { margin-top:11px; font-size:11px; line-height:1.6; max-height:44vh; overflow:auto; }
|
| 67 |
+
#ans .meta { color:#6fa6a0; letter-spacing:.04em; font-size:10px; margin-bottom:8px; }
|
| 68 |
+
#ans .prose { color:#dbeee9; margin:0 0 10px; padding:9px 10px; border-radius:8px;
|
| 69 |
+
background:rgba(57,230,212,.05); border:1px solid rgba(57,230,212,.16); }
|
| 70 |
+
#ans .prose.na { color:#f0d98a; border-color:rgba(240,200,90,.30); background:rgba(240,200,90,.05); }
|
| 71 |
+
#ans .seed { display:flex; gap:8px; align-items:baseline; padding:5px 0; border-top:1px solid rgba(57,230,212,.10); }
|
| 72 |
+
#ans .seed .k { font-size:9px; letter-spacing:.10em; color:#04070b; border-radius:4px; padding:1px 5px; flex:none; }
|
| 73 |
+
#ans .seed .t { color:#cfe9e6; }
|
| 74 |
+
#ans .seed .sc { margin-left:auto; color:#5f9089; font-size:10px; flex:none; }
|
| 75 |
+
#labels { position:fixed; inset:0; z-index:4; pointer-events:none; }
|
| 76 |
+
#labels .lab {
|
| 77 |
+
position:absolute; transform:translate(-50%,-140%); font-size:10px; letter-spacing:.03em;
|
| 78 |
+
color:#eafffb; text-shadow:0 1px 6px #000; white-space:nowrap; max-width:220px; overflow:hidden; text-overflow:ellipsis;
|
| 79 |
+
}
|
| 80 |
</style>
|
| 81 |
</head>
|
| 82 |
<body>
|
|
|
|
| 84 |
<canvas id="gl"></canvas>
|
| 85 |
<canvas id="fb" hidden></canvas>
|
| 86 |
</div>
|
| 87 |
+
<div id="labels"></div>
|
| 88 |
|
| 89 |
<div class="hud">
|
| 90 |
<h1 class="title">HOLOGRAPHIC ESTATE</h1>
|
| 91 |
+
<p class="sub">Point-field estate · live knowledge brain</p>
|
| 92 |
<div class="badges">
|
| 93 |
+
<span class="badge warn">SAMPLE — procedural estate model</span>
|
| 94 |
+
<span class="badge live" id="brainBadge">BRAIN — connecting…</span>
|
| 95 |
</div>
|
| 96 |
</div>
|
| 97 |
|
| 98 |
+
<div id="brain">
|
| 99 |
+
<h2>ASK THE ESTATE BRAIN</h2>
|
| 100 |
+
<p class="hint">Queries the live a11oy knowledge graph. Returns a grounded subgraph
|
| 101 |
+
(nodes are <b>MODELED</b>); prose is shown only when a sovereign model is reachable — never fabricated.</p>
|
| 102 |
+
<div class="row">
|
| 103 |
+
<input id="q" type="text" placeholder="e.g. agentic orchestration, energy defense…" autocomplete="off" />
|
| 104 |
+
<button id="ask">ASK</button>
|
| 105 |
+
</div>
|
| 106 |
+
<div id="ans"></div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
<div class="legend">
|
| 110 |
+
estate structure<span class="dot" style="background:#eafffb"></span><br/>
|
| 111 |
roof line<span class="dot" style="background:#39e6d4"></span><br/>
|
| 112 |
+
grounds<span class="dot" style="background:#2f7fb0"></span><br/>
|
| 113 |
+
knowledge nodes<span class="dot" style="background:#f0c85a"></span><br/>
|
| 114 |
+
<span style="opacity:.7">node layout is deterministic, not measured coordinates</span>
|
| 115 |
</div>
|
| 116 |
|
| 117 |
<div class="status">renderer: <b id="rstat">initialising…</b> · <span id="rnote">building point field</span></div>
|
|
|
|
| 126 |
</script>
|
| 127 |
|
| 128 |
<script>
|
| 129 |
+
// ============================================================================
|
| 130 |
+
// Holographic Estate — procedural estate point field + LIVE knowledge brain.
|
| 131 |
+
// Honest by construction:
|
| 132 |
+
// • the house/grounds are a SAMPLE procedural model, not a real capture;
|
| 133 |
+
// • the constellation nodes come from the live a11oy brain graph (MODELED);
|
| 134 |
+
// • node positions are a deterministic layout, NOT measured coordinates;
|
| 135 |
+
// • generated prose is shown ONLY if the brain reports a reachable model,
|
| 136 |
+
// otherwise we display the grounded subgraph and say prose is UNAVAILABLE.
|
| 137 |
+
// ============================================================================
|
| 138 |
+
const BRAIN = '/api/a11oy/v1/brain'; // same-origin governed brain API
|
| 139 |
+
|
| 140 |
// ---- Shared procedural estate point field (single source for WebGL + 2D fallback) ----
|
|
|
|
| 141 |
function buildEstatePoints() {
|
| 142 |
const pts = []; // {x,y,z, c:[r,g,b]}
|
| 143 |
const push = (x,y,z,c) => pts.push({x,y,z,c});
|
| 144 |
const CYAN = [0.22,0.90,0.83], WHITE = [0.92,1.0,0.98], BLUE = [0.18,0.50,0.69];
|
| 145 |
const jit = (n) => (Math.random()-0.5)*n;
|
| 146 |
|
|
|
|
| 147 |
const W=6, H=3, D=4;
|
| 148 |
const wallDensity = 2600;
|
| 149 |
for (let i=0;i<wallDensity;i++){
|
|
|
|
| 156 |
else { z=D/2; x=(Math.random()-0.5)*W; }
|
| 157 |
push(x+jit(0.03), y+jit(0.03), z+jit(0.03), WHITE);
|
| 158 |
}
|
|
|
|
| 159 |
const roofDensity = 1700, RH=1.8;
|
| 160 |
for (let i=0;i<roofDensity;i++){
|
| 161 |
+
const t = Math.random();
|
| 162 |
+
const along = (Math.random()-0.5)*D;
|
| 163 |
const side = Math.random()<0.5 ? -1 : 1;
|
| 164 |
const x = side * (W/2) * (1-t);
|
| 165 |
const y = H + RH * t;
|
| 166 |
push(x+jit(0.02), y+jit(0.02), along+jit(0.03), CYAN);
|
| 167 |
}
|
|
|
|
| 168 |
for (let i=0;i<260;i++){ push(jit(0.05), H+RH+jit(0.02), (Math.random()-0.5)*D, CYAN); }
|
|
|
|
| 169 |
const winCenters = [[-1.6,1.3],[1.6,1.3],[0,0.9]];
|
| 170 |
for (const [wx,wy] of winCenters){
|
| 171 |
+
for (let i=0;i<230;i++){ push(wx+jit(0.5), wy+jit(0.6), D/2+0.02, WHITE); }
|
|
|
|
|
|
|
| 172 |
}
|
|
|
|
| 173 |
const groundR = 11;
|
| 174 |
for (let i=0;i<3200;i++){
|
| 175 |
const r = Math.pow(Math.random(),0.6)*groundR;
|
| 176 |
const a = Math.random()*Math.PI*2;
|
| 177 |
const x = Math.cos(a)*r, z = Math.sin(a)*r;
|
|
|
|
| 178 |
if (Math.abs(x)<W/2+0.3 && Math.abs(z)<D/2+0.3) continue;
|
| 179 |
push(x, jit(0.04), z, BLUE);
|
| 180 |
}
|
|
|
|
| 181 |
const trees = [[-7,-6],[7,-6.5],[-6.5,6],[7.5,6]];
|
| 182 |
for (const [tx,tz] of trees){
|
| 183 |
for (let i=0;i<420;i++){
|
|
|
|
| 192 |
const ESTATE = buildEstatePoints();
|
| 193 |
const rstat = document.getElementById('rstat');
|
| 194 |
const rnote = document.getElementById('rnote');
|
| 195 |
+
const labelsBox = document.getElementById('labels');
|
| 196 |
+
|
| 197 |
+
// ---- Knowledge brain state (populated from the live API; never fabricated) ----
|
| 198 |
+
const KIND_COLORS = {
|
| 199 |
+
axis: [0.24,0.92,0.84],
|
| 200 |
+
surface: [0.30,0.62,0.86],
|
| 201 |
+
estate: [0.95,1.00,0.98],
|
| 202 |
+
repo: [0.96,0.78,0.35],
|
| 203 |
+
person: [0.72,0.55,0.96],
|
| 204 |
+
};
|
| 205 |
+
const kindColor = (k) => KIND_COLORS[k] || [0.55,0.78,0.74];
|
| 206 |
+
const brainNodes = []; // {id,title,kind,pos:{x,y,z}}
|
| 207 |
+
const nodeById = new Map();
|
| 208 |
+
let highlightSet = new Set();
|
| 209 |
+
const QUERY_ANCHOR = { x:0, y:7.2, z:0 }; // where the query beam originates
|
| 210 |
+
|
| 211 |
+
function hashStr(s){ let h=2166136261>>>0; for(let i=0;i<s.length;i++){ h^=s.charCodeAt(i); h=Math.imul(h,16777619);} return h>>>0; }
|
| 212 |
+
function layoutPos(id){
|
| 213 |
+
const h = hashStr(id);
|
| 214 |
+
const a = (h % 4096)/4096 * Math.PI*2;
|
| 215 |
+
const r = 7.5 + ((h>>>12) % 1000)/1000 * 5.5; // radius 7.5 .. 13
|
| 216 |
+
const y = 3.8 + ((h>>>22) % 1000)/1000 * 5.0; // height 3.8 .. 8.8
|
| 217 |
+
return { x: Math.cos(a)*r, y, z: Math.sin(a)*r };
|
| 218 |
+
}
|
| 219 |
+
function addNode(n){
|
| 220 |
+
if (!n || !n.id || nodeById.has(n.id)) return nodeById.get(n && n.id);
|
| 221 |
+
const node = { id:n.id, title:n.title||n.id, kind:n.kind||'node', pos:layoutPos(n.id) };
|
| 222 |
+
nodeById.set(node.id, node); brainNodes.push(node);
|
| 223 |
+
return node;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
// viz adapter is filled by whichever renderer wins (WebGL or 2D fallback)
|
| 227 |
+
const viz = { rebuildNodes(){}, applyHighlight(){}, project:null };
|
| 228 |
+
|
| 229 |
+
async function jget(url){
|
| 230 |
+
const r = await fetch(url, { headers:{ 'accept':'application/json' } });
|
| 231 |
+
if (!r.ok) throw new Error('HTTP '+r.status);
|
| 232 |
+
return r.json();
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
async function initBrain(){
|
| 236 |
+
const badge = document.getElementById('brainBadge');
|
| 237 |
+
try {
|
| 238 |
+
const stats = await jget(`${BRAIN}/stats`);
|
| 239 |
+
const nodes = (stats.node_count||0).toLocaleString();
|
| 240 |
+
const links = (stats.link_count||0).toLocaleString();
|
| 241 |
+
badge.textContent = `BRAIN LIVE · ${nodes} nodes · ${links} links`;
|
| 242 |
+
badge.classList.add('live');
|
| 243 |
+
} catch (e) {
|
| 244 |
+
badge.textContent = 'BRAIN OFFLINE'; badge.classList.remove('live'); badge.classList.add('warn');
|
| 245 |
+
}
|
| 246 |
+
// seed the constellation with the most salient nodes (honest MODELED ranking)
|
| 247 |
+
try {
|
| 248 |
+
const sal = await jget(`${BRAIN}/salience?top=52`);
|
| 249 |
+
for (const r of (sal.ranking||[])) addNode(r);
|
| 250 |
+
viz.rebuildNodes();
|
| 251 |
+
} catch (e) { /* estate still renders; constellation just stays empty */ }
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function esc(s){ return String(s).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); }
|
| 255 |
+
|
| 256 |
+
async function ask(q){
|
| 257 |
+
const ansEl = document.getElementById('ans');
|
| 258 |
+
const btn = document.getElementById('ask');
|
| 259 |
+
q = (q||'').trim(); if (!q) return;
|
| 260 |
+
btn.disabled = true; ansEl.innerHTML = '<div class="meta">querying the estate brain…</div>';
|
| 261 |
+
try {
|
| 262 |
+
const d = await jget(`${BRAIN}/ask?q=${encodeURIComponent(q)}`);
|
| 263 |
+
const seeds = (d.seeds||[]).slice(0, 8);
|
| 264 |
+
// register + highlight the grounded subgraph
|
| 265 |
+
highlightSet = new Set();
|
| 266 |
+
for (const s of seeds){ const n = addNode(s); if (n) highlightSet.add(n.id); }
|
| 267 |
+
viz.rebuildNodes(); viz.applyHighlight();
|
| 268 |
|
| 269 |
+
const model = d.answer_model || null;
|
| 270 |
+
const prose = (d.answer || '').trim();
|
| 271 |
+
let html = `<div class="meta">retrieval · ${esc(d.retrieval || 'graph grounding')}</div>`;
|
| 272 |
+
if (model && prose){
|
| 273 |
+
html += `<p class="prose">${esc(prose)}<br><span style="opacity:.6;font-size:9px">MODELED · ${esc(model)}</span></p>`;
|
| 274 |
+
} else {
|
| 275 |
+
html += `<p class="prose na">PROSE UNAVAILABLE — no sovereign model reachable. Showing the grounded subgraph below (this is the honest result, not a fabricated answer).</p>`;
|
| 276 |
+
}
|
| 277 |
+
if (seeds.length){
|
| 278 |
+
for (const s of seeds){
|
| 279 |
+
const c = kindColor(s.kind).map(v=>Math.round(v*255));
|
| 280 |
+
const sc = (typeof s.score==='number') ? s.score.toFixed(3) : '';
|
| 281 |
+
html += `<div class="seed"><span class="k" style="background:rgb(${c[0]},${c[1]},${c[2]})">${esc(s.kind||'node')}</span>`
|
| 282 |
+
+ `<span class="t">${esc(s.title||s.id)}</span><span class="sc">${sc}</span></div>`;
|
| 283 |
+
}
|
| 284 |
+
} else {
|
| 285 |
+
html += `<div class="meta">no grounding nodes returned.</div>`;
|
| 286 |
+
}
|
| 287 |
+
ansEl.innerHTML = html;
|
| 288 |
+
} catch (e) {
|
| 289 |
+
ansEl.innerHTML = `<div class="meta" style="color:#f0d98a">brain unreachable (${esc(e.message)}). The estate model is still live above.</div>`;
|
| 290 |
+
} finally { btn.disabled = false; }
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
document.getElementById('ask').addEventListener('click', ()=> ask(document.getElementById('q').value));
|
| 294 |
+
document.getElementById('q').addEventListener('keydown', (e)=>{ if (e.key==='Enter') ask(e.target.value); });
|
| 295 |
+
|
| 296 |
+
// ---- floating labels for highlighted nodes (shared by both render paths) ----
|
| 297 |
+
const labelDivs = new Map();
|
| 298 |
+
function syncLabels(){
|
| 299 |
+
for (const [id,div] of labelDivs){ if (!highlightSet.has(id)){ div.remove(); labelDivs.delete(id); } }
|
| 300 |
+
for (const id of highlightSet){
|
| 301 |
+
if (!labelDivs.has(id)){
|
| 302 |
+
const n = nodeById.get(id); if (!n) continue;
|
| 303 |
+
const div = document.createElement('div'); div.className='lab';
|
| 304 |
+
div.textContent = n.title.length>34 ? n.title.slice(0,33)+'…' : n.title;
|
| 305 |
+
labelsBox.appendChild(div); labelDivs.set(id, div);
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
function positionLabels(){
|
| 310 |
+
if (!viz.project) return;
|
| 311 |
+
for (const [id,div] of labelDivs){
|
| 312 |
+
const n = nodeById.get(id); if (!n){ continue; }
|
| 313 |
+
const p = viz.project(n.pos.x, n.pos.y, n.pos.z);
|
| 314 |
+
if (p && p.vis){ div.style.display='block'; div.style.left=p.sx+'px'; div.style.top=p.sy+'px'; }
|
| 315 |
+
else { div.style.display='none'; }
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
// ---- 2D never-blank fallback (estate + constellation, shared field) ----
|
| 320 |
function run2DFallback(reason){
|
| 321 |
const glc = document.getElementById('gl'); glc.hidden = true;
|
| 322 |
const c = document.getElementById('fb'); c.hidden = false;
|
| 323 |
const ctx = c.getContext('2d');
|
| 324 |
+
rstat.textContent = '2D fallback'; rstat.style.color = '#f0d98a';
|
|
|
|
| 325 |
rnote.textContent = reason || 'WebGL2 unavailable — projecting same model';
|
| 326 |
let W,H,DPR;
|
| 327 |
function resize(){
|
|
|
|
| 330 |
c.width = W*DPR; c.height = H*DPR; ctx.setTransform(DPR,0,0,DPR,0,0);
|
| 331 |
}
|
| 332 |
window.addEventListener('resize', resize); resize();
|
| 333 |
+
let ang = 0; const focal = 620, camY = 2.6, camDist = 18; const cx = ()=>W/2, cy = ()=>H/2 + 40;
|
| 334 |
+
function proj3(x,y,z){
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
const ca=Math.cos(ang), sa=Math.sin(ang);
|
| 336 |
+
const rx = x*ca - z*sa; const rz = x*sa + z*ca + camDist; const ry = y - camY;
|
| 337 |
+
if (rz<=0.1) return null; const s = focal/rz;
|
| 338 |
+
return { sx: cx() + rx*s, sy: cy() - ry*s, z: rz, s };
|
| 339 |
+
}
|
| 340 |
+
viz.project = (x,y,z)=>{ const p=proj3(x,y,z); return p ? {sx:p.sx, sy:p.sy, vis:true} : {vis:false}; };
|
| 341 |
+
viz.rebuildNodes = ()=>{}; viz.applyHighlight = ()=>{ syncLabels(); };
|
| 342 |
+
function frame(){
|
| 343 |
+
ang += 0.0030; ctx.clearRect(0,0,W,H);
|
| 344 |
const proj = [];
|
| 345 |
+
for (const p of ESTATE){ const q=proj3(p.x,p.y,p.z); if(q){ q.c=p.c; q.kind='estate'; proj.push(q);} }
|
| 346 |
+
for (const n of brainNodes){ const q=proj3(n.pos.x,n.pos.y,n.pos.z); if(q){ q.c=kindColor(n.kind); q.node=n; proj.push(q);} }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
proj.sort((a,b)=>b.z-a.z);
|
| 348 |
+
// query beams to highlighted seeds
|
| 349 |
+
if (highlightSet.size){
|
| 350 |
+
const a0 = proj3(QUERY_ANCHOR.x,QUERY_ANCHOR.y,QUERY_ANCHOR.z);
|
| 351 |
+
if (a0){ ctx.strokeStyle='rgba(57,230,212,.28)'; ctx.lineWidth=1;
|
| 352 |
+
for (const n of brainNodes){ if(!highlightSet.has(n.id)) continue; const b=proj3(n.pos.x,n.pos.y,n.pos.z); if(b){ ctx.beginPath(); ctx.moveTo(a0.sx,a0.sy); ctx.lineTo(b.sx,b.sy); ctx.stroke(); } } }
|
| 353 |
+
}
|
| 354 |
for (const q of proj){
|
| 355 |
+
const hot = q.node && highlightSet.has(q.node.id);
|
| 356 |
+
const r = q.node ? (hot? Math.max(2.6,q.s*0.10) : Math.max(1.2,q.s*0.05)) : Math.max(0.35, q.s*0.05);
|
| 357 |
+
const a = q.node ? (hot?0.95:0.6) : Math.min(0.85, 34/q.z);
|
| 358 |
ctx.beginPath();
|
| 359 |
ctx.fillStyle = `rgba(${(q.c[0]*255)|0},${(q.c[1]*255)|0},${(q.c[2]*255)|0},${a})`;
|
| 360 |
+
ctx.arc(q.sx, q.sy, r, 0, Math.PI*2); ctx.fill();
|
|
|
|
| 361 |
}
|
| 362 |
+
positionLabels();
|
| 363 |
requestAnimationFrame(frame);
|
| 364 |
}
|
| 365 |
frame();
|
| 366 |
}
|
| 367 |
|
| 368 |
+
// ---- WebGL2 path (soft gaussian estate + constellation + bloom + orbit) ----
|
| 369 |
async function runWebGL(){
|
| 370 |
const canvas = document.getElementById('gl');
|
| 371 |
const gl2 = canvas.getContext('webgl2');
|
|
|
|
| 386 |
camera.position.set(13, 8, 15);
|
| 387 |
|
| 388 |
const controls = new OrbitControls(camera, renderer.domElement);
|
| 389 |
+
controls.enableDamping = true; controls.autoRotate = true; controls.autoRotateSpeed = 0.5;
|
| 390 |
+
controls.target.set(0, 2.4, 0); controls.minDistance = 8; controls.maxDistance = 46;
|
| 391 |
|
| 392 |
+
// estate points
|
| 393 |
const n = ESTATE.length;
|
| 394 |
+
const positions = new Float32Array(n*3), colors = new Float32Array(n*3);
|
| 395 |
+
for (let i=0;i<n;i++){ const p=ESTATE[i];
|
|
|
|
|
|
|
| 396 |
positions[i*3]=p.x; positions[i*3+1]=p.y; positions[i*3+2]=p.z;
|
| 397 |
+
colors[i*3]=p.c[0]; colors[i*3+1]=p.c[1]; colors[i*3+2]=p.c[2]; }
|
|
|
|
| 398 |
const geo = new THREE.BufferGeometry();
|
| 399 |
geo.setAttribute('position', new THREE.BufferAttribute(positions,3));
|
| 400 |
geo.setAttribute('color', new THREE.BufferAttribute(colors,3));
|
| 401 |
|
| 402 |
+
const pointMat = (size, alpha) => new THREE.ShaderMaterial({
|
| 403 |
+
uniforms:{ uSize:{value:size}, uPixelRatio:{value:renderer.getPixelRatio()}, uAlpha:{value:alpha} },
|
| 404 |
+
transparent:true, depthWrite:false, blending:THREE.AdditiveBlending, vertexColors:true,
|
| 405 |
+
vertexShader:`uniform float uSize; uniform float uPixelRatio; attribute float aScale; varying vec3 vColor; varying float vScale;
|
| 406 |
+
void main(){ vColor=color; vScale=aScale; vec4 mv=modelViewMatrix*vec4(position,1.0);
|
| 407 |
+
gl_PointSize=uSize*uPixelRatio*(1.0/-mv.z)*4.0*aScale; gl_Position=projectionMatrix*mv; }`,
|
| 408 |
+
fragmentShader:`varying vec3 vColor; varying float vScale; uniform float uAlpha;
|
| 409 |
+
void main(){ vec2 d=gl_PointCoord-vec2(0.5); float r=dot(d,d);
|
| 410 |
+
float a=exp(-r*7.0)*uAlpha*(0.6+0.4*vScale); if(a<0.01) discard; gl_FragColor=vec4(vColor,a); }`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
});
|
| 412 |
+
|
| 413 |
+
const estateScale = new Float32Array(n).fill(1.0);
|
| 414 |
+
geo.setAttribute('aScale', new THREE.BufferAttribute(estateScale,1));
|
| 415 |
+
scene.add(new THREE.Points(geo, pointMat(16.0, 0.30)));
|
| 416 |
+
|
| 417 |
+
// constellation (rebuilt as brain nodes arrive / highlight changes)
|
| 418 |
+
let cGeo=null, cPoints=null, linkObj=null;
|
| 419 |
+
const cMat = pointMat(30.0, 0.9);
|
| 420 |
+
function rebuildNodes(){
|
| 421 |
+
if (cPoints){ scene.remove(cPoints); cGeo.dispose(); }
|
| 422 |
+
const m = brainNodes.length; if (!m){ cPoints=null; return; }
|
| 423 |
+
const cp=new Float32Array(m*3), cc=new Float32Array(m*3), cs=new Float32Array(m);
|
| 424 |
+
for (let i=0;i<m;i++){ const nd=brainNodes[i]; const col=kindColor(nd.kind); const hot=highlightSet.has(nd.id);
|
| 425 |
+
cp[i*3]=nd.pos.x; cp[i*3+1]=nd.pos.y; cp[i*3+2]=nd.pos.z;
|
| 426 |
+
const boost = hot?1.7:1.0; cc[i*3]=Math.min(1,col[0]*boost); cc[i*3+1]=Math.min(1,col[1]*boost); cc[i*3+2]=Math.min(1,col[2]*boost);
|
| 427 |
+
cs[i]= hot?2.4:0.9; }
|
| 428 |
+
cGeo=new THREE.BufferGeometry();
|
| 429 |
+
cGeo.setAttribute('position', new THREE.BufferAttribute(cp,3));
|
| 430 |
+
cGeo.setAttribute('color', new THREE.BufferAttribute(cc,3));
|
| 431 |
+
cGeo.setAttribute('aScale', new THREE.BufferAttribute(cs,1));
|
| 432 |
+
cPoints=new THREE.Points(cGeo, cMat); scene.add(cPoints);
|
| 433 |
+
}
|
| 434 |
+
function applyHighlight(){
|
| 435 |
+
rebuildNodes(); syncLabels();
|
| 436 |
+
if (linkObj){ scene.remove(linkObj); linkObj.geometry.dispose(); linkObj=null; }
|
| 437 |
+
const hi=[...highlightSet].map(id=>nodeById.get(id)).filter(Boolean); if(!hi.length) return;
|
| 438 |
+
const seg=new Float32Array(hi.length*6);
|
| 439 |
+
for (let i=0;i<hi.length;i++){ const p=hi[i].pos;
|
| 440 |
+
seg[i*6]=QUERY_ANCHOR.x; seg[i*6+1]=QUERY_ANCHOR.y; seg[i*6+2]=QUERY_ANCHOR.z;
|
| 441 |
+
seg[i*6+3]=p.x; seg[i*6+4]=p.y; seg[i*6+5]=p.z; }
|
| 442 |
+
const lg=new THREE.BufferGeometry(); lg.setAttribute('position', new THREE.BufferAttribute(seg,3));
|
| 443 |
+
linkObj=new THREE.LineSegments(lg, new THREE.LineBasicMaterial({ color:0x39e6d4, transparent:true, opacity:0.32 }));
|
| 444 |
+
scene.add(linkObj);
|
| 445 |
+
}
|
| 446 |
+
viz.rebuildNodes = rebuildNodes; viz.applyHighlight = applyHighlight;
|
| 447 |
+
viz.project = (x,y,z)=>{ const v=new THREE.Vector3(x,y,z).project(camera);
|
| 448 |
+
return { sx:(v.x*0.5+0.5)*window.innerWidth, sy:(-v.y*0.5+0.5)*window.innerHeight, vis:(v.z<1 && v.z>-1) }; };
|
| 449 |
|
| 450 |
const composer = new EffectComposer(renderer);
|
| 451 |
composer.addPass(new RenderPass(scene, camera));
|
| 452 |
+
composer.addPass(new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.7, 0.6, 0.25));
|
|
|
|
|
|
|
| 453 |
|
| 454 |
window.addEventListener('resize', ()=>{
|
| 455 |
camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix();
|
|
|
|
| 458 |
});
|
| 459 |
|
| 460 |
rstat.textContent = 'WebGL2'; rstat.style.color = '#9fe6dd';
|
| 461 |
+
rnote.textContent = `three r${THREE.REVISION} · estate + live brain constellation`;
|
| 462 |
|
| 463 |
+
rebuildNodes();
|
| 464 |
+
(function loop(){ controls.update(); composer.render(); positionLabels(); requestAnimationFrame(loop); })();
|
| 465 |
}
|
| 466 |
|
| 467 |
+
runWebGL().then(initBrain).catch(err => {
|
| 468 |
run2DFallback(err && err.message === 'no-webgl2'
|
| 469 |
? 'WebGL2 unavailable — projecting same model'
|
| 470 |
: 'renderer offline — projecting same model');
|
| 471 |
+
initBrain();
|
| 472 |
});
|
| 473 |
</script>
|
| 474 |
</body>
|