feat: complete revolution architecture modernization

- Decoupled robust frontend into an Astro framework in `frontend/`.
- Replaced direct WebSocket broadcast with Smart Routing to distribute workload only to idle capable nodes, preventing 503 errors and duplicate responses.
- Rewrote WASM panic points (`unwrap()` handling) into panic-safe match blocks in qwen_coder.rs preventing Node Web Workers from crashing.
- Integrated robust dynamic Three.js 3D visualization.
- Resolved mermaid and THREE.js frontend hydration issues.
This commit is contained in:
Jaakko Vanhala
2026-04-09 16:38:24 +03:00
parent 84b78eb9c6
commit 857afbe111
98 changed files with 26637 additions and 40 deletions

View File

@@ -0,0 +1,6 @@
---
// AgentChat Placeholder
---
<div id="astro-agent-chat">
<!-- Chat component will go here -->
</div>

View File

@@ -0,0 +1,215 @@
---
// Network3D.astro - Visualizes the Agentic Network connections
---
<div id="network-canvas-container"></div>
<style>
#network-canvas-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
pointer-events: none;
opacity: 0.6;
}
</style>
<script>
import * as THREE from 'three';
const container = document.getElementById('network-canvas-container');
if (container) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
// Kipinän brändivärit
const palette = [
new THREE.Color(0x58a6ff), // Sininen
new THREE.Color(0xff6b00), // Oranssi
new THREE.Color(0x2ea043), // Vihreä
new THREE.Color(0xd29922), // Keltainen
new THREE.Color(0x8b949e) // Harmaa
];
// 1. Luodaan perusnoodit (partikkelit)
const geometry = new THREE.BufferGeometry();
const particlesCount = 120;
const posArray = new Float32Array(particlesCount * 3);
const colorArray = new Float32Array(particlesCount * 3);
for (let i = 0; i < particlesCount; i++) {
// Levitä pallomaisesti
posArray[i*3] = (Math.random() - 0.5) * 12;
posArray[i*3+1] = (Math.random() - 0.5) * 12;
posArray[i*3+2] = (Math.random() - 0.5) * 12;
// Valitse satunnainen väri paletista
const color = palette[Math.floor(Math.random() * palette.length)];
colorArray[i*3] = color.r;
colorArray[i*3+1] = color.g;
colorArray[i*3+2] = color.b;
}
geometry.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colorArray, 3));
const material = new THREE.PointsMaterial({
size: 0.08,
vertexColors: true,
transparent: true,
opacity: 0.8
});
const particlesMesh = new THREE.Points(geometry, material);
scene.add(particlesMesh);
// 2. Yhdistävät viivat (vain visualisointiin, himmeät)
const lineMaterial = new THREE.LineBasicMaterial({
vertexColors: true,
transparent: true,
opacity: 0.15,
blending: THREE.AdditiveBlending
});
const lineGeometry = new THREE.BufferGeometry();
lineGeometry.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
lineGeometry.setAttribute('color', new THREE.BufferAttribute(colorArray, 3));
const linesMesh = new THREE.LineSegments(lineGeometry, lineMaterial);
particlesMesh.add(linesMesh);
// 3. Räjähdykset ja kipinät (Sparks)
const maxSparks = 1000;
const sparkGeometry = new THREE.BufferGeometry();
const sparkPositions = new Float32Array(maxSparks * 3);
const sparkColors = new Float32Array(maxSparks * 3);
const sparkVelocities = new Float32Array(maxSparks * 3);
const sparkLifetimes = new Float32Array(maxSparks);
// Piilota aluksi kaikki kipinät kauas
for(let i=0; i<maxSparks*3; i++) sparkPositions[i] = 1000;
sparkGeometry.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
sparkGeometry.setAttribute('color', new THREE.BufferAttribute(sparkColors, 3));
const sparkMaterial = new THREE.PointsMaterial({
size: 0.04,
vertexColors: true,
transparent: true,
opacity: 1.0,
blending: THREE.AdditiveBlending
});
const sparksMesh = new THREE.Points(sparkGeometry, sparkMaterial);
particlesMesh.add(sparksMesh); // Lisätään pääverkon sisään jotta pyörii sen mukana
camera.position.z = 6;
function triggerExplosion() {
// Valitse satunnainen noodisolmu lähtöpisteeksi
const nodeIdx = Math.floor(Math.random() * particlesCount);
const nx = posArray[nodeIdx * 3];
const ny = posArray[nodeIdx * 3 + 1];
const nz = posArray[nodeIdx * 3 + 2];
const nr = colorArray[nodeIdx * 3];
const ng = colorArray[nodeIdx * 3 + 1];
const nb = colorArray[nodeIdx * 3 + 2];
const sparkCount = 15 + Math.random() * 25; // 15-40 kipinää
let spawned = 0;
for(let i=0; i<maxSparks; i++) {
if(sparkLifetimes[i] <= 0) {
sparkLifetimes[i] = 0.5 + Math.random() * 1.5; // Elinikä 0.5-2sek
// Alkusijainti on noodi itse
sparkPositions[i*3] = nx;
sparkPositions[i*3+1] = ny;
sparkPositions[i*3+2] = nz;
// Satunnainen lentosuunta ja voima (pallomaisesti)
const u = Math.random();
const v = Math.random();
const theta = u * 2.0 * Math.PI;
const phi = Math.acos(2.0 * v - 1.0);
const speed = 2.0 + Math.random() * 3.0; // Nopeus
sparkVelocities[i*3] = Math.sin(phi) * Math.cos(theta) * speed;
sparkVelocities[i*3+1] = Math.sin(phi) * Math.sin(theta) * speed;
sparkVelocities[i*3+2] = Math.cos(phi) * speed;
// Kipinän väri (noodin väri + satunnaista vaaleutta)
sparkColors[i*3] = Math.min(1.0, nr + Math.random() * 0.5);
sparkColors[i*3+1] = Math.min(1.0, ng + Math.random() * 0.5);
sparkColors[i*3+2] = Math.min(1.0, nb + Math.random() * 0.5);
spawned++;
if(spawned >= sparkCount) break;
}
}
}
// 4. Animaatiolooppi
let lastTime = performance.now();
function animate(time: number) {
requestAnimationFrame(animate);
const dt = (time - lastTime) / 1000; // sekunteja
lastTime = time;
if (dt > 0.1) return; // vältä lagipiikkien aiheuttamat hyperhypyt
const elapsedTime = time / 1000;
particlesMesh.rotation.y = elapsedTime * 0.05;
particlesMesh.rotation.x = elapsedTime * 0.02;
// Arvo satunnaisia räjähdyksiä (n. 1 per sekunti)
if (Math.random() < 1.0 * dt) {
triggerExplosion();
}
// Päivitä kipinät
let sparksUpdated = false;
for(let i=0; i<maxSparks; i++) {
if(sparkLifetimes[i] > 0) {
sparkLifetimes[i] -= dt;
if(sparkLifetimes[i] <= 0) {
sparkPositions[i*3] = 1000; // Piilota kuollessaan
} else {
// Liikuta nopeuden mukaan
sparkPositions[i*3] += sparkVelocities[i*3] * dt;
sparkPositions[i*3+1] += sparkVelocities[i*3+1] * dt;
sparkPositions[i*3+2] += sparkVelocities[i*3+2] * dt;
// Painovoimaefekti / ilmanvastus
sparkVelocities[i*3+1] -= 2.0 * dt; // Painovoima
sparkVelocities[i*3] *= 0.98; // Ilmanvastus
sparkVelocities[i*3+1] *= 0.98;
sparkVelocities[i*3+2] *= 0.98;
}
sparksUpdated = true;
}
}
if (sparksUpdated) {
sparkGeometry.attributes.position.needsUpdate = true;
sparkGeometry.attributes.color.needsUpdate = true;
}
renderer.render(scene, camera);
}
requestAnimationFrame((time) => { lastTime = time; requestAnimationFrame(animate); });
// Pakotettu uudelleenkokoyritys
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
}
</script>