betterwithage Claude Opus 4.7 commited on
Commit
e665504
·
verified ·
1 Parent(s): 0c94fe6

deploy(hf): sync szl-holdings/a11oy@main derived COPY set

Browse files

Reusable 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>

Files changed (1) hide show
  1. 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:56px; z-index:5; font-size:10.5px; letter-spacing:.08em;
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 model · Holo Lab</p>
53
  <div class="badges">
54
- <span class="badge warn">SAMPLE — procedural model, not a real capture</span>
55
- <span class="badge">PREVIEW</span>
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(); // 0..1 across span
102
- const along = (Math.random()-0.5)*D; // along ridge (z)
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
- // ---- 2D never-blank fallback (source-over, small points, shared field) ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const focal = 620, camY = 2.2, camDist = 18;
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
- // depth sort (paint far first)
 
 
 
 
 
 
 
166
  const proj = [];
167
- for (const p of ESTATE){
168
- const x = p.x*ca - p.z*sa;
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 r = Math.max(0.35, q.s*0.05);
178
- const a = Math.min(0.85, 34/q.z);
 
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 points + bloom + orbit), falls back on any failure ----
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.55;
211
- controls.target.set(0, 1.6, 0); controls.minDistance = 8; controls.maxDistance = 40;
212
 
 
213
  const n = ESTATE.length;
214
- const positions = new Float32Array(n*3);
215
- const colors = new Float32Array(n*3);
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 mat = new THREE.ShaderMaterial({
226
- uniforms: { uSize:{value:16.0}, uPixelRatio:{value:renderer.getPixelRatio()} },
227
- transparent:true, depthWrite:false, blending:THREE.AdditiveBlending,
228
- vertexColors:true,
229
- vertexShader:`
230
- uniform float uSize; uniform float uPixelRatio;
231
- varying vec3 vColor;
232
- void main(){
233
- vColor = color;
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
- scene.add(new THREE.Points(geo, mat));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
  const composer = new EffectComposer(renderer);
251
  composer.addPass(new RenderPass(scene, camera));
252
- const bloom = new UnrealBloomPass(
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} · ${n.toLocaleString()} points · soft-gaussian + bloom`;
264
 
265
- (function loop(){ controls.update(); composer.render(); requestAnimationFrame(loop); })();
 
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 => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[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>