Compare commits
10 Commits
gemini/iss
...
pr-1139
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3fddc0585 | ||
|
|
84f75e1e51 | ||
|
|
4d9fd555a0 | ||
| d09b31825b | |||
| 475df10944 | |||
| b4afcd40ce | |||
| d71628e087 | |||
| 6ae5e40cc7 | |||
| 518717f820 | |||
| 309f07166c |
36
DESIGN.md
Normal file
36
DESIGN.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Project Mnemosyne: Spatial Memory Schema
|
||||
|
||||
## Overview
|
||||
Project Mnemosyne transforms the Nexus from a static environment into a real-time visualization of Timmy's holographic memory (fact_store).
|
||||
|
||||
## 1. Room Mapping (Categories)
|
||||
Memory categories are mapped to specific spatial zones in the Nexus to provide cognitive structure.
|
||||
|
||||
| Category | Nexus Room | Visual Theme |
|
||||
| :--- | :--- | :--- |
|
||||
| user_pref | The Library | Archive shelves, soft lighting, velvet |
|
||||
| project | The Workshop | Drafting tables, holographic blueprints, metal |
|
||||
| tool | The Armory | Server racks, neon circuitry, chrome |
|
||||
| general | The Commons | Open garden, floating islands, eclectic |
|
||||
|
||||
## 2. Object Mapping (Facts)
|
||||
Every single fact in the holographic memory is represented as a 3D object.
|
||||
|
||||
- **Object Type:** Depending on the fact's content, it is rendered as a primitive (sphere, cube, pyramid) or a specific asset.
|
||||
- **Luminosity (Trust):** The glow intensity of the object is tied to its trust score (0.0 - 1.0).
|
||||
- **Scale (Importance):** The size of the object is tied to the number of relations it has to other facts.
|
||||
|
||||
## 3. Spatial Distribution (Coordinates)
|
||||
Coordinates are calculated based on semantic adjacency.
|
||||
|
||||
- **Clustering:** Facts that are 'related' (via the fact_store adjacency graph) are placed closer together.
|
||||
- **Layout Algorithm:** A 3D force-directed graph layout is used within each room.
|
||||
- **Centroid:** The most 'central' fact (highest connectivity) is placed at the room's center.
|
||||
|
||||
## 4. Lifecycle Events
|
||||
The Nexus responds to real-time memory updates:
|
||||
- **FACT_CREATED:** A new object fades into existence at the calculated coordinate.
|
||||
- **FACT_UPDATED:** The object pulses and adjusts its luminosity/scale.
|
||||
- **FACT_REMOVED:** The object dissolves into particles.
|
||||
- **FACT_RECALLED:** The object emits a beam of light toward the User/Agent, signaling active use.
|
||||
|
||||
756
app.js
756
app.js
@@ -22,6 +22,146 @@ const NEXUS = {
|
||||
}
|
||||
};
|
||||
|
||||
class MnemosyneManager {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.schema = null;
|
||||
this.memoryObjects = new Map(); // factId -> Mesh
|
||||
this.rooms = {}; // category -> { center, color }
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
const response = await fetch('./mnemosyne_schema.json');
|
||||
this.schema = await response.json();
|
||||
this._setupRooms();
|
||||
console.log('Mnemosyne initialized');
|
||||
} catch (e) {
|
||||
console.error('Mnemosyne init failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
_setupRooms() {
|
||||
const roomConfigs = this.schema.rooms;
|
||||
const roomNames = Object.keys(roomConfigs);
|
||||
|
||||
// Arrange rooms in a circle around the center
|
||||
const radius = 15;
|
||||
roomNames.forEach((cat, i) => {
|
||||
const angle = (i / roomNames.length) * Math.PI * 2;
|
||||
this.rooms[cat] = {
|
||||
name: roomConfigs[cat].name,
|
||||
center: new THREE.Vector3(Math.cos(angle) * radius, 0, Math.sin(angle) * radius),
|
||||
color: new THREE.Color(roomConfigs[cat].visual_accent)
|
||||
};
|
||||
|
||||
// Add a visual marker for the room (a floating holographic sign)
|
||||
this._createRoomMarker(cat);
|
||||
});
|
||||
}
|
||||
|
||||
_createRoomMarker(cat) {
|
||||
const room = this.rooms[cat];
|
||||
const group = new THREE.Group();
|
||||
|
||||
const geo = new THREE.TorusGeometry(2, 0.05, 16, 100);
|
||||
const mat = new THREE.MeshBasicMaterial({ color: room.color, transparent: true, opacity: 0.4 });
|
||||
const ring = new THREE.Mesh(geo, mat);
|
||||
ring.rotation.x = Math.PI / 2;
|
||||
group.add(ring);
|
||||
|
||||
group.position.copy(room.center);
|
||||
group.position.y = 0.1;
|
||||
this.scene.add(group);
|
||||
}
|
||||
|
||||
spawnFact(fact) {
|
||||
const { id, category, trust, importance, content } = fact;
|
||||
const room = this.rooms[category];
|
||||
if (!room) return;
|
||||
|
||||
// Visuals based on schema
|
||||
const geo = this._getGeometryForFact(content);
|
||||
const mat = new THREE.MeshPhysicalMaterial({
|
||||
color: room.color,
|
||||
emissive: room.color,
|
||||
emissiveIntensity: trust * 2,
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
transmission: 0.5,
|
||||
thickness: 1,
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geo, mat);
|
||||
|
||||
// Position: random jitter around room center
|
||||
mesh.position.copy(room.center);
|
||||
mesh.position.x += (Math.random() - 0.5) * 4;
|
||||
mesh.position.z += (Math.random() - 0.5) * 4;
|
||||
mesh.position.y = 1 + Math.random() * 2;
|
||||
|
||||
// Scale based on importance
|
||||
const s = 0.1 + importance * 0.4;
|
||||
mesh.scale.setScalar(s);
|
||||
|
||||
this.memoryObjects.set(id, mesh);
|
||||
this.scene.add(mesh);
|
||||
|
||||
// Animation: Fade in
|
||||
mesh.scale.setScalar(0);
|
||||
new Promise(resolve => {
|
||||
const start = performance.now();
|
||||
const duration = 1000;
|
||||
const animate = (now) => {
|
||||
const progress = Math.min((now - start) / duration, 1);
|
||||
mesh.scale.setScalar(s * progress);
|
||||
if (progress < 1) requestAnimationFrame(animate);
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
});
|
||||
}
|
||||
|
||||
_getGeometryForFact(content) {
|
||||
const rand = Math.random();
|
||||
if (rand < 0.33) return new THREE.SphereGeometry(0.5, 16, 16);
|
||||
if (rand < 0.66) return new THREE.BoxGeometry(0.5, 0.5, 0.5);
|
||||
return new THREE.ConeGeometry(0.4, 0.7, 16);
|
||||
}
|
||||
|
||||
updateFact(fact) {
|
||||
const mesh = this.memoryObjects.get(fact.id);
|
||||
if (!mesh) return;
|
||||
|
||||
// Update luminosity based on trust
|
||||
mesh.material.emissiveIntensity = fact.trust * 2;
|
||||
|
||||
// Update scale based on importance
|
||||
const s = 0.1 + fact.importance * 0.4;
|
||||
mesh.scale.setScalar(s);
|
||||
}
|
||||
|
||||
removeFact(id) {
|
||||
const mesh = this.memoryObjects.get(id);
|
||||
if (!mesh) return;
|
||||
|
||||
// Animation: Fade out/shrink
|
||||
const start = performance.now();
|
||||
const duration = 500;
|
||||
const originalScale = mesh.scale.x;
|
||||
const animate = (now) => {
|
||||
const progress = Math.min((now - start) / duration, 1);
|
||||
mesh.scale.setScalar(originalScale * (1 - progress));
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
this.scene.remove(mesh);
|
||||
this.memoryObjects.delete(id);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ STATE ═══
|
||||
let camera, scene, renderer, composer;
|
||||
let clock, playerPos, playerRot;
|
||||
@@ -39,12 +179,31 @@ let thoughtStreamMesh;
|
||||
let harnessPulseMesh;
|
||||
let powerMeterBars = [];
|
||||
let particles, dustParticles;
|
||||
let dualBrainGroup, dualBrainScanCtx, dualBrainScanTexture;
|
||||
let cloudOrb, localOrb, cloudOrbLight, localOrbLight, dualBrainLight;
|
||||
let debugOverlay;
|
||||
let glassEdgeMaterials = []; // Glass tile edge materials for animation
|
||||
let voidLight = null; // Point light below glass floor
|
||||
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
||||
let chatOpen = true;
|
||||
let loadProgress = 0;
|
||||
let performanceTier = 'high';
|
||||
|
||||
// ═══ COMMIT HEATMAP ═══
|
||||
let heatmapMesh = null;
|
||||
let heatmapMat = null;
|
||||
let heatmapTexture = null;
|
||||
const _heatmapCanvas = document.createElement('canvas');
|
||||
_heatmapCanvas.width = 512;
|
||||
_heatmapCanvas.height = 512;
|
||||
const HEATMAP_ZONES = [
|
||||
{ name: 'Claude', color: [255, 100, 60], authorMatch: /^claude$/i, angleDeg: 0 },
|
||||
{ name: 'Timmy', color: [ 60, 160, 255], authorMatch: /^timmy/i, angleDeg: 90 },
|
||||
{ name: 'Kimi', color: [ 60, 255, 140], authorMatch: /^kimi/i, angleDeg: 180 },
|
||||
{ name: 'Perplexity', color: [200, 60, 255], authorMatch: /^perplexity/i, angleDeg: 270 },
|
||||
];
|
||||
const _heatZoneIntensity = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));
|
||||
|
||||
// ═══ NAVIGATION SYSTEM ═══
|
||||
const NAV_MODES = ['walk', 'orbit', 'fly'];
|
||||
let navModeIdx = 0;
|
||||
@@ -80,6 +239,9 @@ async function init() {
|
||||
updateLoad(10);
|
||||
|
||||
scene = new THREE.Scene();
|
||||
window.mnemosyne = new MnemosyneManager(scene);
|
||||
await mnemosyne.init();
|
||||
simulateMemoryStream();
|
||||
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
@@ -92,6 +254,7 @@ async function init() {
|
||||
createLighting();
|
||||
updateLoad(40);
|
||||
createFloor();
|
||||
createCommitHeatmap();
|
||||
updateLoad(50);
|
||||
createBatcaveTerminal();
|
||||
updateLoad(60);
|
||||
@@ -124,6 +287,7 @@ async function init() {
|
||||
createThoughtStream();
|
||||
createHarnessPulse();
|
||||
createSessionPowerMeter();
|
||||
createDualBrainPanel();
|
||||
updateLoad(90);
|
||||
|
||||
composer = new EffectComposer(renderer);
|
||||
@@ -303,10 +467,13 @@ function createLighting() {
|
||||
// ═══ FLOOR ═══
|
||||
function createFloor() {
|
||||
const platGeo = new THREE.CylinderGeometry(25, 25, 0.3, 6);
|
||||
const platMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x0a0f1a,
|
||||
roughness: 0.8,
|
||||
metalness: 0.3,
|
||||
const platMat = new THREE.MeshPhysicalMaterial({
|
||||
color: NEXUS.colors.bg,
|
||||
transparent: true,
|
||||
opacity: 0.2,
|
||||
transmission: 0.9,
|
||||
roughness: 0.1,
|
||||
metalness: 0.2,
|
||||
});
|
||||
const platform = new THREE.Mesh(platGeo, platMat);
|
||||
platform.position.y = -0.15;
|
||||
@@ -330,6 +497,238 @@ function createFloor() {
|
||||
ring.rotation.x = Math.PI / 2;
|
||||
ring.position.y = 0.05;
|
||||
scene.add(ring);
|
||||
|
||||
// ─── Glass floor sections showing void below ───
|
||||
_buildGlassFloor();
|
||||
}
|
||||
|
||||
function _buildGlassFloor() {
|
||||
const GLASS_TILE_SIZE = 0.85;
|
||||
const GLASS_TILE_GAP = 0.14;
|
||||
const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP;
|
||||
const GLASS_RADIUS = 4.55;
|
||||
|
||||
const glassPlatformGroup = new THREE.Group();
|
||||
|
||||
// Solid dark frame ring around the glass section
|
||||
const frameMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x0a1828,
|
||||
metalness: 0.9,
|
||||
roughness: 0.1,
|
||||
emissive: new THREE.Color(NEXUS.colors.primary).multiplyScalar(0.06),
|
||||
});
|
||||
const rimGeo = new THREE.RingGeometry(4.7, 5.3, 64);
|
||||
const rim = new THREE.Mesh(rimGeo, frameMat);
|
||||
rim.rotation.x = -Math.PI / 2;
|
||||
rim.position.y = 0.01;
|
||||
glassPlatformGroup.add(rim);
|
||||
|
||||
const borderTorusGeo = new THREE.TorusGeometry(5.0, 0.1, 6, 64);
|
||||
const borderTorus = new THREE.Mesh(borderTorusGeo, frameMat);
|
||||
borderTorus.rotation.x = Math.PI / 2;
|
||||
borderTorus.position.y = 0.01;
|
||||
glassPlatformGroup.add(borderTorus);
|
||||
|
||||
// Semi-transparent glass tile material (transmission lets void show through)
|
||||
const glassTileMat = new THREE.MeshPhysicalMaterial({
|
||||
color: new THREE.Color(NEXUS.colors.primary),
|
||||
transparent: true,
|
||||
opacity: 0.09,
|
||||
roughness: 0.0,
|
||||
metalness: 0.0,
|
||||
transmission: 0.92,
|
||||
thickness: 0.06,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
|
||||
// Collect tile positions within the glass radius
|
||||
const tileSlots = [];
|
||||
for (let row = -5; row <= 5; row++) {
|
||||
for (let col = -5; col <= 5; col++) {
|
||||
const x = col * GLASS_TILE_STEP;
|
||||
const z = row * GLASS_TILE_STEP;
|
||||
const distFromCenter = Math.sqrt(x * x + z * z);
|
||||
if (distFromCenter > GLASS_RADIUS) continue;
|
||||
tileSlots.push({ x, z, distFromCenter });
|
||||
}
|
||||
}
|
||||
|
||||
// InstancedMesh for all tiles (single draw call)
|
||||
const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE);
|
||||
const tileMesh = new THREE.InstancedMesh(tileGeo, glassTileMat, tileSlots.length);
|
||||
tileMesh.instanceMatrix.setUsage(THREE.StaticDrawUsage);
|
||||
const dummy = new THREE.Object3D();
|
||||
dummy.rotation.x = -Math.PI / 2;
|
||||
for (let i = 0; i < tileSlots.length; i++) {
|
||||
dummy.position.set(tileSlots[i].x, 0.005, tileSlots[i].z);
|
||||
dummy.updateMatrix();
|
||||
tileMesh.setMatrixAt(i, dummy.matrix);
|
||||
}
|
||||
tileMesh.instanceMatrix.needsUpdate = true;
|
||||
glassPlatformGroup.add(tileMesh);
|
||||
|
||||
// Merge all tile edge lines into a single LineSegments draw call
|
||||
const HS = GLASS_TILE_SIZE / 2;
|
||||
const edgeVerts = new Float32Array(tileSlots.length * 8 * 3);
|
||||
let evi = 0;
|
||||
for (const { x, z } of tileSlots) {
|
||||
const y = 0.008;
|
||||
edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS;
|
||||
edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS;
|
||||
edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS;
|
||||
edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS;
|
||||
edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS;
|
||||
edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS;
|
||||
edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS;
|
||||
edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS;
|
||||
}
|
||||
const mergedEdgeGeo = new THREE.BufferGeometry();
|
||||
mergedEdgeGeo.setAttribute('position', new THREE.BufferAttribute(edgeVerts, 3));
|
||||
const edgeMat = new THREE.LineBasicMaterial({
|
||||
color: NEXUS.colors.primary,
|
||||
transparent: true,
|
||||
opacity: 0.55,
|
||||
});
|
||||
glassPlatformGroup.add(new THREE.LineSegments(mergedEdgeGeo, edgeMat));
|
||||
|
||||
// Register per-tile edge entries for the animation loop
|
||||
// (we animate the single merged material, grouped by distance bands)
|
||||
const BAND_COUNT = 6;
|
||||
const bandMats = [];
|
||||
for (let b = 0; b < BAND_COUNT; b++) {
|
||||
const mat = new THREE.LineBasicMaterial({
|
||||
color: NEXUS.colors.primary,
|
||||
transparent: true,
|
||||
opacity: 0.55,
|
||||
});
|
||||
// distFromCenter goes 0 → GLASS_RADIUS; spread across bands
|
||||
const distFromCenter = (b / (BAND_COUNT - 1)) * GLASS_RADIUS;
|
||||
glassEdgeMaterials.push({ mat, distFromCenter });
|
||||
bandMats.push(mat);
|
||||
}
|
||||
|
||||
// Rebuild edge geometry per band so each band has its own material
|
||||
// (more draw calls but proper animated glow rings)
|
||||
mergedEdgeGeo.dispose(); // dispose the merged one we won't use
|
||||
for (let b = 0; b < BAND_COUNT; b++) {
|
||||
const bandMin = (b / BAND_COUNT) * GLASS_RADIUS;
|
||||
const bandMax = ((b + 1) / BAND_COUNT) * GLASS_RADIUS;
|
||||
const bandSlots = tileSlots.filter(s => s.distFromCenter >= bandMin && s.distFromCenter < bandMax);
|
||||
if (bandSlots.length === 0) continue;
|
||||
|
||||
const bVerts = new Float32Array(bandSlots.length * 8 * 3);
|
||||
let bvi = 0;
|
||||
for (const { x, z } of bandSlots) {
|
||||
const y = 0.008;
|
||||
bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS;
|
||||
bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS;
|
||||
bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS;
|
||||
bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS;
|
||||
bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS;
|
||||
bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS;
|
||||
bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS;
|
||||
bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS;
|
||||
}
|
||||
const bGeo = new THREE.BufferGeometry();
|
||||
bGeo.setAttribute('position', new THREE.BufferAttribute(bVerts, 3));
|
||||
glassPlatformGroup.add(new THREE.LineSegments(bGeo, bandMats[b]));
|
||||
}
|
||||
|
||||
// Void light pulses below the glass to illuminate the emptiness underneath
|
||||
voidLight = new THREE.PointLight(NEXUS.colors.primary, 0.5, 14);
|
||||
voidLight.position.set(0, -3.5, 0);
|
||||
glassPlatformGroup.add(voidLight);
|
||||
|
||||
scene.add(glassPlatformGroup);
|
||||
}
|
||||
|
||||
// ═══ COMMIT HEATMAP FUNCTIONS ═══
|
||||
function createCommitHeatmap() {
|
||||
heatmapTexture = new THREE.CanvasTexture(_heatmapCanvas);
|
||||
heatmapMat = new THREE.MeshBasicMaterial({
|
||||
map: heatmapTexture,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
heatmapMesh = new THREE.Mesh(new THREE.CircleGeometry(24, 64), heatmapMat);
|
||||
heatmapMesh.rotation.x = -Math.PI / 2;
|
||||
heatmapMesh.position.y = 0.005;
|
||||
scene.add(heatmapMesh);
|
||||
// Kick off first fetch; subsequent updates every 5 min
|
||||
updateHeatmap();
|
||||
setInterval(updateHeatmap, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
function drawHeatmap() {
|
||||
const ctx = _heatmapCanvas.getContext('2d');
|
||||
const cx = 256, cy = 256, r = 246;
|
||||
const SPAN = Math.PI / 2;
|
||||
ctx.clearRect(0, 0, 512, 512);
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
for (const zone of HEATMAP_ZONES) {
|
||||
const intensity = _heatZoneIntensity[zone.name] || 0;
|
||||
if (intensity < 0.01) continue;
|
||||
const [rr, gg, bb] = zone.color;
|
||||
const baseRad = zone.angleDeg * (Math.PI / 180);
|
||||
const gx = cx + Math.cos(baseRad) * r * 0.55;
|
||||
const gy = cy + Math.sin(baseRad) * r * 0.55;
|
||||
const grad = ctx.createRadialGradient(gx, gy, 0, gx, gy, r * 0.75);
|
||||
grad.addColorStop(0, `rgba(${rr},${gg},${bb},${0.65 * intensity})`);
|
||||
grad.addColorStop(0.45, `rgba(${rr},${gg},${bb},${0.25 * intensity})`);
|
||||
grad.addColorStop(1, `rgba(${rr},${gg},${bb},0)`);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, cy);
|
||||
ctx.arc(cx, cy, r, baseRad - SPAN / 2, baseRad + SPAN / 2);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
if (intensity > 0.05) {
|
||||
ctx.font = `bold ${Math.round(13 * intensity + 7)}px "Courier New", monospace`;
|
||||
ctx.fillStyle = `rgba(${rr},${gg},${bb},${Math.min(intensity * 1.2, 0.9)})`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(zone.name, cx + Math.cos(baseRad) * r * 0.62, cy + Math.sin(baseRad) * r * 0.62);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
if (heatmapTexture) heatmapTexture.needsUpdate = true;
|
||||
}
|
||||
|
||||
async function updateHeatmap() {
|
||||
let commits = [];
|
||||
try {
|
||||
const res = await fetch(
|
||||
'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=50',
|
||||
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
|
||||
);
|
||||
if (res.ok) commits = await res.json();
|
||||
} catch { /* network error — use zero baseline */ }
|
||||
|
||||
const DECAY_MS = 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
const raw = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));
|
||||
for (const commit of commits) {
|
||||
const author = commit.commit?.author?.name || commit.author?.login || '';
|
||||
const ts = new Date(commit.commit?.author?.date || 0).getTime();
|
||||
const age = now - ts;
|
||||
if (age > DECAY_MS) continue;
|
||||
const weight = 1 - age / DECAY_MS;
|
||||
for (const zone of HEATMAP_ZONES) {
|
||||
if (zone.authorMatch.test(author)) { raw[zone.name] += weight; break; }
|
||||
}
|
||||
}
|
||||
const MAX_W = 8;
|
||||
for (const zone of HEATMAP_ZONES) {
|
||||
_heatZoneIntensity[zone.name] = Math.min(raw[zone.name] / MAX_W, 1.0);
|
||||
}
|
||||
drawHeatmap();
|
||||
}
|
||||
|
||||
// ═══ BATCAVE TERMINAL ═══
|
||||
@@ -677,6 +1076,183 @@ function createSessionPowerMeter() {
|
||||
scene.add(group);
|
||||
}
|
||||
|
||||
// ═══ DUAL-BRAIN PANEL ═══
|
||||
function createDualBrainTexture() {
|
||||
const W = 512, H = 512;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 6, 20, 0.90)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
ctx.strokeStyle = '#4488ff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(1, 1, W - 2, H - 2);
|
||||
|
||||
ctx.strokeStyle = '#223366';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(5, 5, W - 10, H - 10);
|
||||
|
||||
ctx.font = 'bold 22px "Courier New", monospace';
|
||||
ctx.fillStyle = '#88ccff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('\u25C8 DUAL-BRAIN STATUS', W / 2, 40);
|
||||
|
||||
ctx.strokeStyle = '#1a3a6a';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(20, 52);
|
||||
ctx.lineTo(W - 20, 52);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.font = '11px "Courier New", monospace';
|
||||
ctx.fillStyle = '#556688';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText('BRAIN GAP SCORECARD', 20, 74);
|
||||
|
||||
const categories = [
|
||||
{ name: 'Triage' },
|
||||
{ name: 'Tool Use' },
|
||||
{ name: 'Code Gen' },
|
||||
{ name: 'Planning' },
|
||||
{ name: 'Communication' },
|
||||
{ name: 'Reasoning' },
|
||||
];
|
||||
|
||||
const barX = 20;
|
||||
const barW = W - 130;
|
||||
const barH = 20;
|
||||
let y = 90;
|
||||
|
||||
for (const cat of categories) {
|
||||
ctx.font = '13px "Courier New", monospace';
|
||||
ctx.fillStyle = '#445566';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(cat.name, barX, y + 14);
|
||||
|
||||
ctx.font = 'bold 13px "Courier New", monospace';
|
||||
ctx.fillStyle = '#334466';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText('\u2014', W - 20, y + 14);
|
||||
|
||||
y += 22;
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';
|
||||
ctx.fillRect(barX, y, barW, barH);
|
||||
|
||||
y += barH + 12;
|
||||
}
|
||||
|
||||
ctx.strokeStyle = '#1a3a6a';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(20, y + 4);
|
||||
ctx.lineTo(W - 20, y + 4);
|
||||
ctx.stroke();
|
||||
|
||||
y += 22;
|
||||
|
||||
ctx.font = 'bold 18px "Courier New", monospace';
|
||||
ctx.fillStyle = '#334466';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('AWAITING DEPLOYMENT', W / 2, y + 10);
|
||||
|
||||
ctx.font = '11px "Courier New", monospace';
|
||||
ctx.fillStyle = '#223344';
|
||||
ctx.fillText('Dual-brain system not yet connected', W / 2, y + 32);
|
||||
|
||||
y += 52;
|
||||
ctx.beginPath();
|
||||
ctx.arc(W / 2 - 60, y + 8, 6, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#334466';
|
||||
ctx.fill();
|
||||
ctx.font = '11px "Courier New", monospace';
|
||||
ctx.fillStyle = '#334466';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText('CLOUD', W / 2 - 48, y + 12);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(W / 2 + 30, y + 8, 6, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#334466';
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#334466';
|
||||
ctx.fillText('LOCAL', W / 2 + 42, y + 12);
|
||||
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
}
|
||||
|
||||
function createDualBrainPanel() {
|
||||
dualBrainGroup = new THREE.Group();
|
||||
dualBrainGroup.position.set(10, 3, -8);
|
||||
dualBrainGroup.lookAt(0, 3, 0);
|
||||
scene.add(dualBrainGroup);
|
||||
|
||||
// Main panel sprite
|
||||
const panelTexture = createDualBrainTexture();
|
||||
const panelMat = new THREE.SpriteMaterial({
|
||||
map: panelTexture, transparent: true, opacity: 0.92, depthWrite: false,
|
||||
});
|
||||
const panelSprite = new THREE.Sprite(panelMat);
|
||||
panelSprite.scale.set(5.0, 5.0, 1);
|
||||
panelSprite.position.set(0, 0, 0);
|
||||
panelSprite.userData = { baseY: 0, floatPhase: 0, floatSpeed: 0.22, zoomLabel: 'Dual-Brain Status' };
|
||||
dualBrainGroup.add(panelSprite);
|
||||
|
||||
// Panel glow light
|
||||
dualBrainLight = new THREE.PointLight(0x4488ff, 0.6, 10);
|
||||
dualBrainLight.position.set(0, 0.5, 1);
|
||||
dualBrainGroup.add(dualBrainLight);
|
||||
|
||||
// Cloud Brain Orb
|
||||
const cloudOrbGeo = new THREE.SphereGeometry(0.35, 32, 32);
|
||||
const cloudOrbMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x334466,
|
||||
emissive: new THREE.Color(0x334466),
|
||||
emissiveIntensity: 0.1, metalness: 0.3, roughness: 0.2,
|
||||
transparent: true, opacity: 0.85,
|
||||
});
|
||||
cloudOrb = new THREE.Mesh(cloudOrbGeo, cloudOrbMat);
|
||||
cloudOrb.position.set(-2.0, 3.0, 0);
|
||||
cloudOrb.userData.zoomLabel = 'Cloud Brain';
|
||||
dualBrainGroup.add(cloudOrb);
|
||||
|
||||
cloudOrbLight = new THREE.PointLight(0x334466, 0.15, 5);
|
||||
cloudOrbLight.position.copy(cloudOrb.position);
|
||||
dualBrainGroup.add(cloudOrbLight);
|
||||
|
||||
// Local Brain Orb
|
||||
const localOrbGeo = new THREE.SphereGeometry(0.35, 32, 32);
|
||||
const localOrbMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x334466,
|
||||
emissive: new THREE.Color(0x334466),
|
||||
emissiveIntensity: 0.1, metalness: 0.3, roughness: 0.2,
|
||||
transparent: true, opacity: 0.85,
|
||||
});
|
||||
localOrb = new THREE.Mesh(localOrbGeo, localOrbMat);
|
||||
localOrb.position.set(2.0, 3.0, 0);
|
||||
localOrb.userData.zoomLabel = 'Local Brain';
|
||||
dualBrainGroup.add(localOrb);
|
||||
|
||||
localOrbLight = new THREE.PointLight(0x334466, 0.15, 5);
|
||||
localOrbLight.position.copy(localOrb.position);
|
||||
dualBrainGroup.add(localOrbLight);
|
||||
|
||||
// Scan line overlay
|
||||
const scanCanvas = document.createElement('canvas');
|
||||
scanCanvas.width = 512;
|
||||
scanCanvas.height = 512;
|
||||
dualBrainScanCtx = scanCanvas.getContext('2d');
|
||||
dualBrainScanTexture = new THREE.CanvasTexture(scanCanvas);
|
||||
const scanMat = new THREE.SpriteMaterial({
|
||||
map: dualBrainScanTexture, transparent: true, opacity: 0.18, depthWrite: false,
|
||||
});
|
||||
const scanSprite = new THREE.Sprite(scanMat);
|
||||
scanSprite.scale.set(5.0, 5.0, 1);
|
||||
scanSprite.position.set(0, 0, 0.01);
|
||||
dualBrainGroup.add(scanSprite);
|
||||
}
|
||||
|
||||
// ═══ VISION SYSTEM ═══
|
||||
function createVisionPoints(data) {
|
||||
data.forEach(config => {
|
||||
@@ -821,6 +1397,33 @@ function createPortal(config) {
|
||||
light.position.set(0, 3.5, 1);
|
||||
group.add(light);
|
||||
|
||||
// Rune Ring (Portal-tethered)
|
||||
const runeCount = 8;
|
||||
const runeRingRadius = 4.5;
|
||||
const runes = [];
|
||||
for (let i = 0; i < runeCount; i++) {
|
||||
const angle = (i / runeCount) * Math.PI * 2;
|
||||
const runeGeo = new THREE.BoxGeometry(0.3, 0.8, 0.1);
|
||||
const runeMat = new THREE.MeshStandardMaterial({
|
||||
color: portalColor,
|
||||
emissive: portalColor,
|
||||
emissiveIntensity: 0.8,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
roughness: 0.2,
|
||||
metalness: 0.5,
|
||||
});
|
||||
const rune = new THREE.Mesh(runeGeo, runeMat);
|
||||
rune.position.set(
|
||||
Math.cos(angle) * runeRingRadius,
|
||||
4,
|
||||
Math.sin(angle) * runeRingRadius
|
||||
);
|
||||
rune.rotation.y = angle + Math.PI / 2;
|
||||
group.add(rune);
|
||||
runes.push(rune);
|
||||
}
|
||||
|
||||
// Label
|
||||
const labelCanvas = document.createElement('canvas');
|
||||
labelCanvas.width = 512;
|
||||
@@ -860,7 +1463,8 @@ function createPortal(config) {
|
||||
ring,
|
||||
swirl,
|
||||
pSystem,
|
||||
light
|
||||
light,
|
||||
runes
|
||||
};
|
||||
}
|
||||
|
||||
@@ -986,20 +1590,7 @@ function createAmbientStructures() {
|
||||
scene.add(crystal);
|
||||
});
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i / 5) * Math.PI * 2;
|
||||
const r = 10;
|
||||
const geo = new THREE.OctahedronGeometry(0.4, 0);
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: NEXUS.colors.primary,
|
||||
emissive: NEXUS.colors.primary,
|
||||
emissiveIntensity: 0.5,
|
||||
});
|
||||
const stone = new THREE.Mesh(geo, mat);
|
||||
stone.position.set(Math.cos(angle) * r, 5 + Math.sin(i * 1.3) * 1.5, Math.sin(angle) * r);
|
||||
stone.name = 'runestone_' + i;
|
||||
scene.add(stone);
|
||||
}
|
||||
|
||||
|
||||
const coreGeo = new THREE.IcosahedronGeometry(0.6, 2);
|
||||
const coreMat = new THREE.MeshPhysicalMaterial({
|
||||
@@ -1312,6 +1903,46 @@ function closeVisionOverlay() {
|
||||
let lastThoughtTime = 0;
|
||||
let pulseTimer = 0;
|
||||
|
||||
|
||||
// ═══ MNEMOSYNE SIMULATION ═══
|
||||
function simulateMemoryStream() {
|
||||
const categories = ['user_pref', 'project', 'tool', 'general'];
|
||||
const facts = [];
|
||||
|
||||
setInterval(() => {
|
||||
const id = 'fact_' + Math.random().toString(36).substr(2, 9);
|
||||
const category = categories[Math.floor(Math.random() * categories.length)];
|
||||
const fact = {
|
||||
id,
|
||||
category,
|
||||
trust: Math.random(),
|
||||
importance: Math.random(),
|
||||
content: 'Simulated holographic memory fragment ' + id
|
||||
};
|
||||
|
||||
// We need access to the mnemosyne instance.
|
||||
// I'll make it a global in app.js for simplicity in this prototype.
|
||||
if (window.mnemosyne) {
|
||||
window.mnemosyne.spawnFact(fact);
|
||||
facts.push(fact);
|
||||
}
|
||||
|
||||
// Cleanup old facts
|
||||
if (facts.length > 30) {
|
||||
const oldFact = facts.shift();
|
||||
if (window.mnemosyne) window.mnemosyne.removeFact(oldFact.id);
|
||||
}
|
||||
|
||||
// Occasionally update a random fact
|
||||
if (Math.random() > 0.7 && facts.length > 0) {
|
||||
const target = facts[Math.floor(Math.random() * facts.length)];
|
||||
target.trust = Math.random();
|
||||
target.importance = Math.random();
|
||||
if (window.mnemosyne) window.mnemosyne.updateFact(target);
|
||||
}
|
||||
}, 7000);
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
requestAnimationFrame(gameLoop);
|
||||
const delta = Math.min(clock.getDelta(), 0.1);
|
||||
@@ -1408,6 +2039,9 @@ function gameLoop() {
|
||||
const sky = scene.getObjectByName('skybox');
|
||||
if (sky) sky.material.uniforms.uTime.value = elapsed;
|
||||
|
||||
// Pulse heatmap opacity
|
||||
if (heatmapMat) heatmapMat.opacity = 0.75 + Math.sin(elapsed * 0.6) * 0.2;
|
||||
|
||||
batcaveTerminals.forEach(t => {
|
||||
if (t.scanMat?.uniforms) t.scanMat.uniforms.uTime.value = elapsed;
|
||||
});
|
||||
@@ -1427,6 +2061,12 @@ function gameLoop() {
|
||||
positions[i * 3 + 1] += Math.sin(elapsed + i) * 0.002;
|
||||
}
|
||||
portal.pSystem.geometry.attributes.position.needsUpdate = true;
|
||||
|
||||
// Animate runes
|
||||
portal.runes.forEach((rune, i) => {
|
||||
rune.position.y = 4 + Math.sin(elapsed * 2 + i * 0.5) * 0.2;
|
||||
rune.rotation.z = elapsed * 0.8 + i;
|
||||
});
|
||||
});
|
||||
|
||||
// Animate Vision Points
|
||||
@@ -1442,6 +2082,34 @@ function gameLoop() {
|
||||
// Animate Agents
|
||||
updateAgents(elapsed, delta);
|
||||
|
||||
// Animate Dual-Brain Panel
|
||||
if (dualBrainGroup) {
|
||||
dualBrainGroup.position.y = 3 + Math.sin(elapsed * 0.22) * 0.15;
|
||||
if (cloudOrb) {
|
||||
cloudOrb.position.y = 3 + Math.sin(elapsed * 1.3) * 0.15;
|
||||
cloudOrb.rotation.y = elapsed * 0.4;
|
||||
}
|
||||
if (localOrb) {
|
||||
localOrb.position.y = 3 + Math.sin(elapsed * 1.3 + Math.PI) * 0.15;
|
||||
localOrb.rotation.y = -elapsed * 0.4;
|
||||
}
|
||||
if (dualBrainLight) {
|
||||
dualBrainLight.intensity = 0.4 + Math.sin(elapsed * 1.5) * 0.2;
|
||||
}
|
||||
if (dualBrainScanCtx && dualBrainScanTexture) {
|
||||
const W = 512, H = 512;
|
||||
dualBrainScanCtx.clearRect(0, 0, W, H);
|
||||
const scanY = ((elapsed * 80) % H);
|
||||
const grad = dualBrainScanCtx.createLinearGradient(0, scanY - 20, 0, scanY + 20);
|
||||
grad.addColorStop(0, 'rgba(68, 136, 255, 0)');
|
||||
grad.addColorStop(0.5, 'rgba(68, 136, 255, 0.6)');
|
||||
grad.addColorStop(1, 'rgba(68, 136, 255, 0)');
|
||||
dualBrainScanCtx.fillStyle = grad;
|
||||
dualBrainScanCtx.fillRect(0, scanY - 20, W, 40);
|
||||
dualBrainScanTexture.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Animate Power Meter
|
||||
powerMeterBars.forEach((bar, i) => {
|
||||
const level = (Math.sin(elapsed * 2 + i * 0.5) * 0.5 + 0.5);
|
||||
@@ -1451,6 +2119,15 @@ function gameLoop() {
|
||||
bar.scale.x = active ? 1.2 : 1.0;
|
||||
});
|
||||
|
||||
// Animate glass floor edge glow (ripple outward from center)
|
||||
for (const { mat, distFromCenter } of glassEdgeMaterials) {
|
||||
const phase = elapsed * 1.1 - distFromCenter * 0.18;
|
||||
mat.opacity = 0.25 + Math.sin(phase) * 0.22;
|
||||
}
|
||||
if (voidLight) {
|
||||
voidLight.intensity = 0.35 + Math.sin(elapsed * 1.4) * 0.2;
|
||||
}
|
||||
|
||||
if (thoughtStreamMesh) {
|
||||
thoughtStreamMesh.material.uniforms.uTime.value = elapsed;
|
||||
thoughtStreamMesh.rotation.y = elapsed * 0.05;
|
||||
@@ -1463,14 +2140,7 @@ function gameLoop() {
|
||||
dustParticles.rotation.y = elapsed * 0.01;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const stone = scene.getObjectByName('runestone_' + i);
|
||||
if (stone) {
|
||||
stone.position.y = 5 + Math.sin(elapsed * 0.8 + i * 1.3) * 0.8;
|
||||
stone.rotation.y = elapsed * 0.5 + i;
|
||||
stone.rotation.x = elapsed * 0.3 + i * 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const core = scene.getObjectByName('nexus-core');
|
||||
if (core) {
|
||||
@@ -1692,4 +2362,36 @@ function triggerHarnessPulse() {
|
||||
}
|
||||
}
|
||||
|
||||
// === BITCOIN BLOCK HEIGHT ===
|
||||
(function initBitcoin() {
|
||||
const blockHeightDisplay = document.getElementById('block-height-display');
|
||||
const blockHeightValue = document.getElementById('block-height-value');
|
||||
if (!blockHeightDisplay || !blockHeightValue) return;
|
||||
|
||||
let lastKnownBlockHeight = null;
|
||||
|
||||
async function fetchBlockHeight() {
|
||||
try {
|
||||
const res = await fetch('https://blockstream.info/api/blocks/tip/height');
|
||||
if (!res.ok) return;
|
||||
const height = parseInt(await res.text(), 10);
|
||||
if (isNaN(height)) return;
|
||||
|
||||
if (lastKnownBlockHeight !== null && height !== lastKnownBlockHeight) {
|
||||
blockHeightDisplay.classList.remove('fresh');
|
||||
void blockHeightDisplay.offsetWidth;
|
||||
blockHeightDisplay.classList.add('fresh');
|
||||
}
|
||||
|
||||
lastKnownBlockHeight = height;
|
||||
blockHeightValue.textContent = height.toLocaleString();
|
||||
} catch (_) {
|
||||
// Network unavailable
|
||||
}
|
||||
}
|
||||
|
||||
fetchBlockHeight();
|
||||
setInterval(fetchBlockHeight, 60000);
|
||||
})();
|
||||
|
||||
init();
|
||||
|
||||
1
icons/icon-192x192.png
Normal file
1
icons/icon-192x192.png
Normal file
@@ -0,0 +1 @@
|
||||
placeholder 192x192
|
||||
1
icons/icon-512x512.png
Normal file
1
icons/icon-512x512.png
Normal file
@@ -0,0 +1 @@
|
||||
placeholder 512x512
|
||||
21
index.html
21
index.html
@@ -23,6 +23,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
@@ -154,6 +155,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bitcoin Block Height -->
|
||||
<div id="block-height-display">
|
||||
<span class="block-height-label">⛏ BLOCK</span>
|
||||
<span id="block-height-value">—</span>
|
||||
</div>
|
||||
|
||||
<!-- Click to Enter -->
|
||||
<div id="enter-prompt" style="display:none;">
|
||||
<div class="enter-content">
|
||||
@@ -172,6 +179,20 @@
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.then(registration => {
|
||||
console.log('Service Worker registered: ', registration);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Service Worker registration failed: ', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Live Refresh: polls Gitea for new commits on main, reloads when SHA changes -->
|
||||
<div id="live-refresh-banner" style="
|
||||
display:none; position:fixed; top:0; left:0; right:0; z-index:9999;
|
||||
|
||||
21
manifest.json
Normal file
21
manifest.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "The Nexus",
|
||||
"short_name": "Nexus",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#000000",
|
||||
"description": "Timmy's Sovereign Home - A Three.js environment.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
mnemosyne_schema.json
Normal file
34
mnemosyne_schema.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"rooms": {
|
||||
"user_pref": {
|
||||
"name": "The Library",
|
||||
"theme": "Archive shelves, soft lighting, velvet",
|
||||
"visual_accent": "#C2B280"
|
||||
},
|
||||
"project": {
|
||||
"name": "The Workshop",
|
||||
"theme": "Drafting tables, holographic blueprints, metal",
|
||||
"visual_accent": "#4682B4"
|
||||
},
|
||||
"tool": {
|
||||
"name": "The Armory",
|
||||
"theme": "Server racks, neon circuitry, chrome",
|
||||
"visual_accent": "#00FF00"
|
||||
},
|
||||
"general": {
|
||||
"name": "The Commons",
|
||||
"theme": "Open garden, floating islands, eclectic",
|
||||
"visual_accent": "#FFD700"
|
||||
}
|
||||
},
|
||||
"object_mapping": {
|
||||
"trust_score": {
|
||||
"property": "luminosity",
|
||||
"range": [0.0, 1.0]
|
||||
},
|
||||
"connectivity": {
|
||||
"property": "scale",
|
||||
"range": [0.5, 2.0]
|
||||
}
|
||||
}
|
||||
}
|
||||
45
service-worker.js
Normal file
45
service-worker.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const CACHE_NAME = 'nexus-cache-v1';
|
||||
const urlsToCache = [
|
||||
'.',
|
||||
'index.html',
|
||||
'style.css',
|
||||
'app.js',
|
||||
'manifest.json'
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
console.log('Opened cache');
|
||||
return cache.addAll(urlsToCache);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return fetch(event.request);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
const cacheWhitelist = [CACHE_NAME];
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheWhitelist.indexOf(cacheName) === -1) {
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
36
style.css
36
style.css
@@ -625,6 +625,42 @@ canvas#nexus-canvas {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* === BITCOIN BLOCK HEIGHT === */
|
||||
#block-height-display {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
z-index: 20;
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--color-primary);
|
||||
background: rgba(0, 0, 8, 0.7);
|
||||
border: 1px solid var(--color-secondary);
|
||||
padding: 4px 10px;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.block-height-label {
|
||||
color: var(--color-text-muted);
|
||||
margin-right: 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
#block-height-value {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
#block-height-display.fresh #block-height-value {
|
||||
animation: block-flash 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes block-flash {
|
||||
0% { color: #ffffff; text-shadow: 0 0 8px #4488ff; }
|
||||
100% { color: var(--color-primary); text-shadow: none; }
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 480px) {
|
||||
.chat-panel {
|
||||
|
||||
Reference in New Issue
Block a user