285 lines
12 KiB
HTML
285 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Agent Framework • 3D Sphere</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||
<style>
|
||
body { margin: 0; overflow: hidden; }
|
||
#canvas-container { width: 100%; height: 100vh; }
|
||
.label {
|
||
position: absolute;
|
||
color: #a1a1aa;
|
||
font-size: 11px;
|
||
pointer-events: none;
|
||
text-shadow: 0 1px 3px rgba(0,0,0,0.9);
|
||
font-family: ui-monospace, monospace;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="bg-zinc-950 text-zinc-200">
|
||
<div class="absolute top-6 left-6 z-10">
|
||
<div>
|
||
<div class="text-2xl font-semibold tracking-tight">Agent Framework</div>
|
||
<div class="text-xs text-zinc-500">3D Phase Visualization</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="canvas-container"></div>
|
||
|
||
<!-- Details Panel -->
|
||
<div class="absolute top-6 right-6 w-80 bg-zinc-900/90 backdrop-blur border border-zinc-800 rounded-3xl p-6 z-20 hidden" id="details-panel">
|
||
<div id="details-content"></div>
|
||
</div>
|
||
|
||
<div class="absolute bottom-6 left-6 text-[10px] text-zinc-500 z-10">
|
||
Drag to rotate • Scroll to zoom • Click nodes
|
||
</div>
|
||
|
||
<script>
|
||
const phaseData = {
|
||
"Research": { prompt: "prompts/research.md", outputs: "SPEC.md", description: "Explores requirements and produces a clear specification." },
|
||
"Design": { prompt: "prompts/design.md", outputs: "DESIGN.md", description: "Produces data models, flows, architecture, and risks." },
|
||
"Implement": { prompt: "prompts/implement.md", outputs: "Code + tests", description: "Builds the solution using SPEC.md and DESIGN.md." },
|
||
"Bug Find": { prompt: "prompts/bug_finder.md", outputs: "BUG_REPORT.md", description: "Adversarially discovers bugs and spec deviations." },
|
||
"Disprove": { prompt: "prompts/disprover.md", outputs: "DISPROVALS.md", description: "Attempts to disprove findings from the bug report." },
|
||
"Referee": { prompt: "prompts/referee.md", outputs: "VERDICT.md", description: "Judges implementation and compares Bug Finder vs Disprover." },
|
||
"Onboard": { prompt: "prompts/onboarding.md", outputs: "Framework files", description: "Initializes the agent framework in a project." },
|
||
"Orchestrate": { prompt: "prompts/orchestrate.md", outputs: "Recommendation", description: "Suggests the next logical phase based on state." },
|
||
"Compaction": { prompt: "prompts/compaction.md", outputs: "Clean rules", description: "Consolidates and deduplicates rules." }
|
||
};
|
||
|
||
const phases = Object.keys(phaseData);
|
||
let scene, camera, renderer;
|
||
let nodes = [];
|
||
let lines = [];
|
||
let raycaster, mouse;
|
||
|
||
function initThreeJS() {
|
||
const container = document.getElementById('canvas-container');
|
||
|
||
scene = new THREE.Scene();
|
||
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
container.appendChild(renderer.domElement);
|
||
|
||
// Sphere
|
||
const sphereGeometry = new THREE.SphereGeometry(2.4, 64, 64);
|
||
const sphereMaterial = new THREE.MeshPhongMaterial({
|
||
color: 0x18181b,
|
||
wireframe: true,
|
||
transparent: true,
|
||
opacity: 0.12
|
||
});
|
||
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
|
||
scene.add(sphere);
|
||
|
||
// Lighting
|
||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||
scene.add(ambientLight);
|
||
const pointLight = new THREE.PointLight(0xffffff, 0.9);
|
||
pointLight.position.set(10, 10, 10);
|
||
scene.add(pointLight);
|
||
|
||
camera.position.z = 6.5;
|
||
|
||
raycaster = new THREE.Raycaster();
|
||
mouse = new THREE.Vector2();
|
||
|
||
// Create nodes + connections
|
||
createNodesOnSphere();
|
||
createConnections();
|
||
|
||
// Events
|
||
window.addEventListener('resize', onWindowResize);
|
||
container.addEventListener('click', onClick);
|
||
|
||
// Orbit controls (simple)
|
||
let isDragging = false;
|
||
let previousMouseX = 0, previousMouseY = 0;
|
||
|
||
container.addEventListener('mousedown', (e) => {
|
||
isDragging = true;
|
||
previousMouseX = e.clientX;
|
||
previousMouseY = e.clientY;
|
||
});
|
||
window.addEventListener('mouseup', () => isDragging = false);
|
||
|
||
container.addEventListener('mousemove', (e) => {
|
||
if (!isDragging) return;
|
||
const deltaMove = { x: e.clientX - previousMouseX, y: e.clientY - previousMouseY };
|
||
|
||
const deltaRotation = new THREE.Quaternion().setFromEuler(
|
||
new THREE.Euler(deltaMove.y * 0.004, deltaMove.x * 0.004, 0, 'XYZ')
|
||
);
|
||
|
||
sphere.quaternion.multiplyQuaternions(deltaRotation, sphere.quaternion);
|
||
|
||
nodes.forEach(n => n.position.applyQuaternion(deltaRotation));
|
||
lines.forEach(line => {
|
||
line.geometry.dispose();
|
||
const newGeo = new THREE.BufferGeometry().setFromPoints([
|
||
line.userData.from.position,
|
||
line.userData.to.position
|
||
]);
|
||
line.geometry = newGeo;
|
||
});
|
||
|
||
previousMouseX = e.clientX;
|
||
previousMouseY = e.clientY;
|
||
});
|
||
|
||
container.addEventListener('wheel', (e) => {
|
||
camera.position.z += e.deltaY * 0.008;
|
||
camera.position.z = Math.max(3.5, Math.min(11, camera.position.z));
|
||
});
|
||
|
||
animate();
|
||
}
|
||
|
||
function createNodesOnSphere() {
|
||
const radius = 2.4;
|
||
const nodeCount = phases.length;
|
||
|
||
phases.forEach((phase, i) => {
|
||
const phi = Math.acos(-1 + (2 * i) / nodeCount);
|
||
const theta = Math.sqrt(nodeCount * Math.PI) * phi;
|
||
|
||
const x = radius * Math.sin(phi) * Math.cos(theta);
|
||
const y = radius * Math.sin(phi) * Math.sin(theta);
|
||
const z = radius * Math.cos(phi);
|
||
|
||
const geometry = new THREE.SphereGeometry(0.11, 16, 16);
|
||
const material = new THREE.MeshPhongMaterial({
|
||
color: 0x3b82f6,
|
||
emissive: 0x1e40af
|
||
});
|
||
const nodeMesh = new THREE.Mesh(geometry, material);
|
||
nodeMesh.position.set(x, y, z);
|
||
nodeMesh.userData = { phase: phase };
|
||
scene.add(nodeMesh);
|
||
nodes.push(nodeMesh);
|
||
|
||
// Label
|
||
const label = document.createElement('div');
|
||
label.className = 'label';
|
||
label.textContent = phase;
|
||
document.getElementById('canvas-container').appendChild(label);
|
||
nodeMesh.userData.label = label;
|
||
});
|
||
}
|
||
|
||
function createConnections() {
|
||
const connections = [
|
||
["Research", "Design"],
|
||
["Design", "Implement"],
|
||
["Implement", "Bug Find"],
|
||
["Bug Find", "Disprove"],
|
||
["Disprove", "Referee"],
|
||
["Referee", "Implement"],
|
||
["Onboard", "Research"],
|
||
["Orchestrate", "Research"],
|
||
["Orchestrate", "Design"],
|
||
["Orchestrate", "Implement"],
|
||
["Orchestrate", "Bug Find"],
|
||
["Orchestrate", "Referee"],
|
||
["Compaction", "Orchestrate"]
|
||
];
|
||
|
||
const lineMaterial = new THREE.LineBasicMaterial({
|
||
color: 0x3f3f46,
|
||
transparent: true,
|
||
opacity: 0.6
|
||
});
|
||
|
||
connections.forEach(([fromName, toName]) => {
|
||
const fromNode = nodes.find(n => n.userData.phase === fromName);
|
||
const toNode = nodes.find(n => n.userData.phase === toName);
|
||
if (!fromNode || !toNode) return;
|
||
|
||
const geometry = new THREE.BufferGeometry().setFromPoints([
|
||
fromNode.position,
|
||
toNode.position
|
||
]);
|
||
const line = new THREE.Line(geometry, lineMaterial);
|
||
line.userData = { from: fromNode, to: toNode };
|
||
scene.add(line);
|
||
lines.push(line);
|
||
});
|
||
}
|
||
|
||
function onClick(event) {
|
||
const rect = renderer.domElement.getBoundingClientRect();
|
||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||
|
||
raycaster.setFromCamera(mouse, camera);
|
||
const intersects = raycaster.intersectObjects(nodes);
|
||
|
||
if (intersects.length > 0) {
|
||
const phase = intersects[0].object.userData.phase;
|
||
showDetails(phase);
|
||
}
|
||
}
|
||
|
||
function showDetails(phase) {
|
||
const panel = document.getElementById('details-panel');
|
||
const data = phaseData[phase];
|
||
|
||
panel.classList.remove('hidden');
|
||
panel.innerHTML = `
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-xl font-semibold">${phase}</div>
|
||
<div class="text-xs text-blue-400 font-mono mt-1">${data.prompt}</div>
|
||
</div>
|
||
<button onclick="hideDetails()" class="text-zinc-500 hover:text-white text-xl">×</button>
|
||
</div>
|
||
|
||
<div class="text-sm leading-relaxed text-zinc-300">${data.description}</div>
|
||
|
||
<div class="mt-6">
|
||
<div class="text-xs uppercase tracking-wider text-zinc-500 mb-2">Outputs</div>
|
||
<div class="text-sm font-mono bg-zinc-950 px-4 py-2 rounded-2xl border border-zinc-800">${data.outputs}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function hideDetails() {
|
||
document.getElementById('details-panel').classList.add('hidden');
|
||
}
|
||
|
||
function onWindowResize() {
|
||
camera.aspect = window.innerWidth / window.innerHeight;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
}
|
||
|
||
function animate() {
|
||
requestAnimationFrame(animate);
|
||
renderer.render(scene, camera);
|
||
|
||
// Update labels
|
||
nodes.forEach(node => {
|
||
if (node.userData.label) {
|
||
const vector = node.position.clone();
|
||
vector.project(camera);
|
||
const x = (vector.x * 0.5 + 0.5) * window.innerWidth;
|
||
const y = (-vector.y * 0.5 + 0.5) * window.innerHeight;
|
||
node.userData.label.style.left = `${x}px`;
|
||
node.userData.label.style.top = `${y}px`;
|
||
}
|
||
});
|
||
}
|
||
|
||
function init() {
|
||
initThreeJS();
|
||
}
|
||
|
||
window.onload = init;
|
||
</script>
|
||
</body>
|
||
</html> |