Skip to content

Instantly share code, notes, and snippets.

@boxabirds
Created March 29, 2025 18:48
Tech Writer Animation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LLM Code Analysis Visualization (Final Layout)</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; background-color: #111; color: #eee; }
canvas { display: block; }
/* Left Info Panel */
#info {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.75);
padding: 15px;
border-radius: 5px;
max-width: 300px; /* Adjusted width */
font-size: 13px;
line-height: 1.4;
z-index: 20;
}
#info h2 { margin-top: 0; }
#status-line { margin-top: 10px; font-weight: bold; }
/* Bottom Left Controls */
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
z-index: 20;
}
button {
margin: 5px; padding: 8px 12px; cursor: pointer; background-color: #444;
color: #eee; border: 1px solid #666; border-radius: 3px;
}
button:hover { background-color: #555; }
button:disabled { background-color: #333; color: #888; cursor: not-allowed; }
/* Bottom Right Legend Panel */
#legend-panel {
position: absolute;
bottom: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 5px;
max-width: 200px;
font-size: 11px;
line-height: 1.3;
z-index: 20;
}
#legend-panel h3 { margin: 0 0 8px 0; font-size: 1.1em; text-align: center; }
#legend-panel ul { list-style: none; padding: 0; margin: 0; }
#legend-panel li { margin-bottom: 3px; display: flex; align-items: center; }
.legend-color {
width: 10px; height: 10px; margin-right: 6px;
border: 1px solid #888; display: inline-block; flex-shrink: 0;
}
/* Top Right ReAct Log Panel */
#react-log-panel {
position: absolute;
top: 10px;
right: 10px;
width: 280px; /* Width of the log panel */
height: calc(100vh - 140px); /* Adjust height dynamically, leaving space for legend/controls */
max-height: 600px; /* Optional max height */
background-color: rgba(10, 10, 20, 0.8); /* Slightly different background */
border: 1px solid #445;
border-radius: 5px;
padding: 10px;
box-sizing: border-box;
overflow-y: auto; /* Enable vertical scrolling */
z-index: 20;
font-size: 12px;
line-height: 1.4;
}
#react-log-panel h2 {
margin: 0 0 10px 0;
font-size: 1.2em;
text-align: center;
color: #aaa;
border-bottom: 1px solid #445;
padding-bottom: 5px;
}
#react-log-list {
list-style: none;
padding: 0;
margin: 0;
}
#react-log-list li {
margin-bottom: 6px;
padding-bottom: 6px;
border-bottom: 1px dashed #334;
word-wrap: break-word; /* Wrap long messages */
}
#react-log-list li:last-child {
border-bottom: none;
margin-bottom: 0;
}
/* Styling for different log types */
.log-start { color: #0f0; } /* Green */
.log-thought { color: #ccc; font-style: italic; } /* Light grey italic */
.log-action { color: #ffae42; } /* Orange */
.log-observation { color: #6495ED; } /* Cornflower blue */
.log-decision { color: #DB7093; font-weight: bold;} /* Pale Violet Red bold */
.log-complete { color: #32CD32; font-weight: bold;} /* Lime Green bold */
.log-error { color: #FF4444; font-weight: bold;} /* Red bold */
/* Tooltip */
.tooltip {
position: absolute; background-color: rgba(50, 50, 50, 0.9); color: white;
padding: 5px 10px; border-radius: 3px; font-size: 11px; white-space: pre-wrap;
pointer-events: none; display: none; max-width: 400px; border: 1px solid #777; z-index: 100;
}
/* Labels */
#labels-container {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; overflow: hidden; z-index: 10;
}
.object-label {
position: absolute; background-color: rgba(0, 0, 0, 0.6); color: #fff;
padding: 3px 6px; border-radius: 3px; font-size: 10px; white-space: nowrap;
transform: translateX(-50%); user-select: none;
}
/* Larger/Bolder Labels */
.object-label-large {
font-size: 13px; /* Increased size */
font-weight: bold;
padding: 4px 8px; /* Slightly larger padding */
}
</style>
</head>
<body>
<!-- UI Elements -->
<div id="info">
<h2>Code Analysis Flow (ReAct)</h2>
<p>Visualizing the interaction between Prompts, LLM, and Tools.</p>
<div id="status-line"><strong>Current Status:</strong> <span id="status">Idle</span></div>
<p>Use mouse to rotate/zoom. Hover for details. Click button below to start.</p>
</div>
<div id="controls">
<button id="startButton">Start Analysis</button>
<button id="resetButton">Reset</button>
</div>
<div id="tooltip" class="tooltip">Tooltip</div>
<div id="labels-container"></div>
<!-- Legend Panel -->
<div id="legend-panel">
<h3>Legend</h3>
<ul>
<li><span class="legend-color" style="background-color: #0077cc;"></span>LLM Core</li>
<li><span class="legend-color" style="background-color: #cc0000;"></span>System Prompt</li>
<li><span class="legend-color" style="background-color: #ff6666;"></span>Sub-Prompt</li>
<li><span class="legend-color" style="background-color: #00cc00;"></span>User Prompt</li>
<li><span class="legend-color" style="background-color: #ffaa00;"></span>Tool</li>
<li><span class="legend-color" style="background-color: #ffff00;"></span>Signal</li>
<li><span class="legend-color" style="background-color: #cc00cc;"></span>Final Answer</li>
<li><span class="legend-color" style="background-color: #aaaaaa; height: 4px; border: none; width: 15px; vertical-align: middle;"></span>Connection</li>
</ul>
</div>
<!-- ReAct Log Panel -->
<div id="react-log-panel">
<h2>ReAct Log</h2>
<ul id="react-log-list"></ul>
</div>
<!-- Libraries -->
<script src="https://unpkg.com/matter-js@0.19.0/build/matter.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.164.1/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
}
}
</script>
<!-- Main Application Logic -->
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// --- Configuration ---
const Colors = {
background: 0x111111, ambientLight: 0x404040, directionalLight: 0xffffff,
llm: 0x0077cc, systemPrompt: 0xcc0000, subPrompt: 0xff6666, userPrompt: 0x00cc00,
tool: 0xffaa00, connection: 0xaaaaaa, signal: 0xffff00, finalAnswer: 0xcc00cc,
};
const Sizes = {
llmRadius: 30, promptSize: 15, subPromptSize: 8, toolSize: 20, signalRadius: 3,
labelOffset: 1.2,
};
// --- Data ---
const PromptData = {
ROLE_AND_TASK: "Expert tech writer role", GENERAL_ANALYSIS_GUIDELINES: "Use tools, be accurate",
INPUT_PROCESSING_GUIDELINES: "Use base dir, handle errors", CODE_ANALYSIS_STRATEGIES: "Explore structure, key files",
REACT_PLANNING_STRATEGY: "ReAct: T->A->O->Repeat", QUALITY_REQUIREMENTS: "MECE analysis, Markdown",
USER_PROMPT_EXAMPLE: "Base directory: /path/to/codebase\n\nAnalyze the main logic." // Shortened for label space
};
const ToolData = {
find_all_matching_files: "Finds files matching pattern",
read_file: "Reads file content",
calculate: "Evaluates math expression" // Shortened for label space
};
// --- Global Variables ---
let scene, camera, renderer, controls;
let engine, world;
let objects = {
llm: null, systemPrompt: null, subPrompts: [], userPrompt: null,
tools: {}, connections: [], signals: [], finalAnswerObj: null
};
let matterBodies = [];
let threeMeshes = [];
let labeledObjects = [];
let constraints = [];
const SIM_STATE = {
IDLE: 'idle', PROMPTS_TO_LLM: 'prompts_to_llm', LLM_THINKING: 'llm_thinking',
LLM_TO_TOOL: 'llm_to_tool', TOOL_PROCESSING: 'tool_processing', TOOL_TO_LLM: 'tool_to_llm',
LLM_DECIDING: 'llm_deciding', FINAL_ANSWER: 'final_answer', COMPLETE: 'complete', ERROR: 'error'
};
let simulationState = SIM_STATE.IDLE;
let currentStep = 0;
const maxSteps = 7;
let currentTool = null;
let lastToolUsed = null;
let tooltipElement, statusElement, labelsContainer, legendPanel, reactLogPanel, reactLogList;
let raycaster, mouse;
const labelUpdateVector = new THREE.Vector3();
let currentTimeoutId = null;
// --- Constants ---
const SIGNAL_DURATION_PROMPT = 350;
const SIGNAL_DURATION_TOOL = 250;
const THINKING_TIME = 150;
// --- Initialization ---
function init() {
console.log("Initializing visualization...");
tooltipElement = document.getElementById('tooltip');
statusElement = document.getElementById('status');
labelsContainer = document.getElementById('labels-container');
legendPanel = document.getElementById('legend-panel');
reactLogPanel = document.getElementById('react-log-panel');
reactLogList = document.getElementById('react-log-list');
setupThreeJS();
setupMatterJS();
setupSceneContent();
setupInteraction();
animate();
updateStatus('Idle. Press "Start Analysis".');
console.log("Initialization complete.");
}
// --- Setup Functions ---
function setupThreeJS() {
console.log("Setting up Three.js...");
scene = new THREE.Scene(); scene.background = new THREE.Color(Colors.background);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 0, 150); // Keep closer zoom
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(Colors.ambientLight));
const dirLight = new THREE.DirectionalLight(Colors.directionalLight, 1.5);
dirLight.position.set(50, 50, 100); scene.add(dirLight);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.dampingFactor = 0.1; controls.screenSpacePanning = false;
controls.minDistance = 30; controls.maxDistance = 500; // Adjust zoom limits
raycaster = new THREE.Raycaster(); mouse = new THREE.Vector2();
window.addEventListener('mousemove', onMouseMove, false); window.addEventListener('resize', onWindowResize, false);
console.log("Three.js setup complete.");
}
function setupMatterJS() {
console.log("Setting up Matter.js...");
if (typeof Matter === 'undefined') { console.error("FATAL: Matter.js global object not found!"); alert("Error: Matter.js library failed to load."); return; }
engine = Matter.Engine.create(); world = engine.world; world.gravity.y = 0;
console.log("Matter.js setup complete.");
}
function setupSceneContent() {
console.log("Setting up scene content...");
try {
// LLM Core
const llmPos = { x: 0, y: 0 };
const llmBody = Matter.Bodies.circle(llmPos.x, llmPos.y, Sizes.llmRadius, { isStatic: true, label: 'LLM Core', restitution: 0.5 });
const llmMesh = createMesh(new THREE.SphereGeometry(Sizes.llmRadius, 32, 32), Colors.llm, llmPos.x, llmPos.y, 0);
if (!llmMesh) throw new Error("Failed to create LLM mesh.");
llmMesh.userData = { info: "Language Model\nProcesses prompts and orchestrates tool use.", body: llmBody };
objects.llm = { mesh: llmMesh, body: llmBody };
addObject(llmMesh, llmBody);
addLabel(llmMesh, "LLM Core", Sizes.llmRadius, true); // Large label
// System Prompt & Sub-Prompts
const sysPromptPos = { x: -75, y: 40 }; // Keep closer position
const sysPromptBody = Matter.Bodies.rectangle(sysPromptPos.x, sysPromptPos.y, Sizes.promptSize * 1.5, Sizes.promptSize * 1.5, { isStatic: true, label: 'System Prompt', restitution: 0.5 });
const sysPromptMesh = createMesh(new THREE.BoxGeometry(Sizes.promptSize, Sizes.promptSize, Sizes.promptSize), Colors.systemPrompt, sysPromptPos.x, sysPromptPos.y, 0);
if (!sysPromptMesh) throw new Error("Failed to create System Prompt mesh.");
if (!PromptData || typeof PromptData !== 'object') throw new Error("PromptData object is missing or invalid.");
const sysPromptInfo = "System Prompt:\n" + Object.values(PromptData).slice(0, -1).join("\n- ");
sysPromptMesh.userData = { info: sysPromptInfo, body: sysPromptBody };
objects.systemPrompt = { mesh: sysPromptMesh, body: sysPromptBody };
addObject(sysPromptMesh, sysPromptBody);
addLabel(sysPromptMesh, "System Prompt", Sizes.promptSize); // Normal label
addConnection(objects.llm, objects.systemPrompt);
const subPromptNames = Object.keys(PromptData).slice(0, -1);
const angleStepSub = (Math.PI * 2) / subPromptNames.length;
subPromptNames.forEach((name, i) => {
const angle = angleStepSub * i + Math.PI / 4;
const radius = Sizes.promptSize * 1.25; // Keep closer radius
const pos = { x: sysPromptPos.x + radius * Math.cos(angle), y: sysPromptPos.y + radius * Math.sin(angle) };
const body = Matter.Bodies.rectangle(pos.x, pos.y, Sizes.subPromptSize * 1.5, Sizes.subPromptSize * 1.5, { isStatic: true, label: `Sub-Prompt: ${name}`, restitution: 0.5 });
const mesh = createMesh(new THREE.BoxGeometry(Sizes.subPromptSize, Sizes.subPromptSize, Sizes.subPromptSize), Colors.subPrompt, pos.x, pos.y, 0);
if (!mesh) throw new Error(`Failed to create Sub-Prompt mesh for ${name}.`);
if (!PromptData[name]) console.warn(`PromptData missing key: ${name}`);
mesh.userData = { info: `Sub-Prompt: ${name}\n${PromptData[name] || 'Info missing'}`, body: body };
const subPromptObj = { mesh: mesh, body: body, name: name };
objects.subPrompts.push(subPromptObj); addObject(mesh, body);
addLabel(mesh, name.replace(/_/g, ' '), Sizes.subPromptSize); // Normal label for sub-prompts
addConnection(objects.systemPrompt, subPromptObj, 0.1);
});
// User Prompt
const userPromptPos = { x: -75, y: -40 }; // Keep closer position
const userPromptBody = Matter.Bodies.rectangle(userPromptPos.x, userPromptPos.y, Sizes.promptSize * 1.5, Sizes.promptSize * 1.5, { isStatic: false, label: 'User Prompt', restitution: 0.5, frictionAir: 0.05 });
const userPromptMesh = createMesh(new THREE.BoxGeometry(Sizes.promptSize, Sizes.promptSize, Sizes.promptSize), Colors.userPrompt, userPromptPos.x, userPromptPos.y, 0);
if (!userPromptMesh) throw new Error("Failed to create User Prompt mesh object.");
if (typeof PromptData.USER_PROMPT_EXAMPLE === 'undefined') console.warn("PromptData.USER_PROMPT_EXAMPLE is undefined!");
userPromptMesh.userData = { info: `User Prompt (Example):\n${PromptData.USER_PROMPT_EXAMPLE || 'Example missing'}`, body: userPromptBody };
objects.userPrompt = { mesh: userPromptMesh, body: userPromptBody };
addObject(userPromptMesh, userPromptBody);
addLabel(userPromptMesh, "User Prompt", Sizes.promptSize, true); // Large label
addConnection(objects.llm, objects.userPrompt);
// Tools (Clustered on the Right)
if (!ToolData || typeof ToolData !== 'object') throw new Error("ToolData object is missing or invalid.");
const toolNames = Object.keys(ToolData); const numTools = toolNames.length;
const toolClusterRadius = 75; // Keep closer radius
const clusterStartAngle = -Math.PI / 2.5; const clusterEndAngle = Math.PI / 2.5;
const totalClusterAngle = clusterEndAngle - clusterStartAngle; const toolAngleStep = numTools > 1 ? totalClusterAngle / (numTools - 1) : 0;
toolNames.forEach((name, i) => {
const angle = clusterStartAngle + (toolAngleStep * i);
const pos = { x: llmPos.x + toolClusterRadius * Math.cos(angle), y: llmPos.y + toolClusterRadius * Math.sin(angle) };
const body = Matter.Bodies.rectangle(pos.x, pos.y, Sizes.toolSize * 1.5, Sizes.toolSize * 1.5, { isStatic: true, label: `Tool: ${name}`, restitution: 0.5 });
let geometry;
if (i % 3 === 0) geometry = new THREE.BoxGeometry(Sizes.toolSize, Sizes.toolSize, Sizes.toolSize);
else if (i % 3 === 1) geometry = new THREE.CylinderGeometry(Sizes.toolSize * 0.7, Sizes.toolSize * 0.7, Sizes.toolSize, 16);
else geometry = new THREE.ConeGeometry(Sizes.toolSize * 0.8, Sizes.toolSize * 1.2, 16);
const mesh = createMesh(geometry, Colors.tool, pos.x, pos.y, 0);
if (!mesh) throw new Error(`Failed to create Tool mesh for ${name}.`);
if (!ToolData[name]) console.warn(`ToolData missing key: ${name}`);
mesh.userData = { info: `Tool: ${name}\n${ToolData[name] || 'Info missing'}`, body: body };
const toolObj = { mesh: mesh, body: body, name: name };
objects.tools[name] = toolObj; addObject(mesh, body);
addLabel(mesh, `Tool: ${name}`, Sizes.toolSize, true); // Large label
addConnection(objects.llm, toolObj);
});
console.log("Tools created.");
} catch (error) { console.error("Error during setupSceneContent:", error); alert(`An error occurred during scene setup: ${error.message}\nCheck console.`); }
console.log("Scene content setup complete.");
}
// --- Helper Functions ---
function createMesh(geometry, color, x, y, z) {
try {
const material = new THREE.MeshStandardMaterial({ color: color, roughness: 0.5, metalness: 0.2, transparent: true, opacity: 0.9 });
const mesh = new THREE.Mesh(geometry, material); mesh.position.set(x, y, z); mesh.castShadow = true; mesh.receiveShadow = true; return mesh;
} catch (error) { console.error("Error in createMesh:", error); return undefined; }
}
function addObject(mesh, body) {
if (!mesh || !body) { console.error("Attempted to add invalid mesh or body:", mesh, body); return; }
scene.add(mesh); Matter.World.add(world, body); matterBodies.push(body); threeMeshes.push(mesh);
}
// Modified addLabel to accept isLarge flag
function addLabel(mesh, text, objectSize, isLarge = false) {
if (!mesh) { console.error("Attempted to add label to invalid mesh for text:", text); return; }
const labelDiv = document.createElement('div');
labelDiv.className = 'object-label';
if (isLarge) {
labelDiv.classList.add('object-label-large'); // Add large class if specified
}
labelDiv.textContent = text;
labelsContainer.appendChild(labelDiv);
labeledObjects.push({ mesh, labelElement: labelDiv, text, size: objectSize });
}
function updateLabels() {
labeledObjects.forEach(item => {
if (!item || !item.mesh || !item.labelElement || !item.mesh.position) return;
const offsetDistance = item.size * Sizes.labelOffset;
labelUpdateVector.set(item.mesh.position.x, item.mesh.position.y + offsetDistance, item.mesh.position.z);
labelUpdateVector.project(camera); const isBehindCamera = labelUpdateVector.z > 1;
const x = (labelUpdateVector.x * 0.5 + 0.5) * renderer.domElement.clientWidth;
const y = (-labelUpdateVector.y * 0.5 + 0.5) * renderer.domElement.clientHeight;
item.labelElement.style.left = `${x}px`; item.labelElement.style.top = `${y}px`;
if (isBehindCamera || x < -100 || x > renderer.domElement.clientWidth + 100 || y < -100 || y > renderer.domElement.clientHeight + 100) {
item.labelElement.style.display = 'none'; } else { item.labelElement.style.display = 'block'; }
});
}
function addConnection(objA, objB, stiffness = 0.01, length = null) {
if (!objA || !objB || !objA.body || !objB.body || !objA.mesh || !objB.mesh) { console.error("Attempted to add connection between invalid objects:", objA, objB); return; }
const bodyA = objA.body; const bodyB = objB.body;
if (!length) { length = Math.hypot(bodyA.position.x - bodyB.position.x, bodyA.position.y - bodyB.position.y); }
const constraint = Matter.Constraint.create({ bodyA, bodyB, stiffness, length, render: { visible: false } });
Matter.World.add(world, constraint); constraints.push(constraint);
const points = [ new THREE.Vector3(), new THREE.Vector3() ]; const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: Colors.connection, transparent: true, opacity: 0.6, linewidth: 4 // Request thicker line
});
const line = new THREE.Line(geometry, material); line.userData.constraint = constraint; scene.add(line); objects.connections.push(line);
}
function setupInteraction() {
document.getElementById('startButton').addEventListener('click', startAnalysis);
document.getElementById('resetButton').addEventListener('click', resetSimulation);
}
// --- ReAct Log Function ---
function logReActStep(type, message) {
if (!reactLogList || !reactLogPanel) return;
const logItem = document.createElement('li');
logItem.classList.add(`log-${type}`);
logItem.textContent = message;
reactLogList.appendChild(logItem);
reactLogPanel.scrollTop = reactLogPanel.scrollHeight;
}
// --- Simulation Control ---
function startAnalysis() {
if (simulationState !== SIM_STATE.IDLE) return;
console.log("Starting analysis simulation...");
resetSimulationVisuals();
simulationState = SIM_STATE.PROMPTS_TO_LLM;
currentStep = 0;
document.getElementById('startButton').disabled = true;
logReActStep('start', 'Analysis Started: Receiving Prompts...');
processCurrentState();
}
function processCurrentState() {
if (currentTimeoutId) { clearTimeout(currentTimeoutId); currentTimeoutId = null; }
if (simulationState !== SIM_STATE.IDLE && simulationState !== SIM_STATE.COMPLETE && simulationState !== SIM_STATE.ERROR) {
if (!objects.llm || !objects.llm.mesh || !objects.llm.body) { return changeState(SIM_STATE.ERROR, "Error: LLM object missing."); }
}
console.log(`Processing State: ${simulationState}, Step: ${currentStep}`);
switch (simulationState) {
case SIM_STATE.PROMPTS_TO_LLM:
updateStatus('System & User Prompts -> LLM');
if (!objects.userPrompt || !objects.userPrompt.body || !objects.systemPrompt || !objects.systemPrompt.body) { return changeState(SIM_STATE.ERROR, "Error: Prompt objects missing."); }
const userPromptBody = objects.userPrompt.body; const llmBody = objects.llm.body;
Matter.Body.setStatic(userPromptBody, false); const forceMagnitude = 0.005 * userPromptBody.mass;
const angle = Math.atan2(llmBody.position.y - userPromptBody.position.y, llmBody.position.x - userPromptBody.position.x);
Matter.Body.applyForce(userPromptBody, userPromptBody.position, { x: forceMagnitude * Math.cos(angle), y: forceMagnitude * Math.sin(angle) });
createSignal(objects.userPrompt, objects.llm, SIGNAL_DURATION_PROMPT, () => { if (simulationState === SIM_STATE.PROMPTS_TO_LLM) { changeState(SIM_STATE.LLM_THINKING); } });
createSignal(objects.systemPrompt, objects.llm, SIGNAL_DURATION_PROMPT, null);
break;
case SIM_STATE.LLM_THINKING:
updateStatus('LLM Thinking...');
logReActStep('thought', 'Planning next action / Selecting tool...');
flashObject(objects.llm.mesh, 1, 150);
currentTimeoutId = setTimeout(() => {
const toolNames = Object.keys(objects.tools);
if (toolNames.length === 0) { console.warn("No tools available."); changeState(SIM_STATE.LLM_DECIDING); }
else {
currentTool = toolNames[Math.floor(Math.random() * toolNames.length)];
if (!objects.tools[currentTool] || !objects.tools[currentTool].mesh) { changeState(SIM_STATE.ERROR, `Error: Invalid tool selected: ${currentTool}`); }
else { changeState(SIM_STATE.LLM_TO_TOOL); }
}
}, THINKING_TIME);
break;
case SIM_STATE.LLM_TO_TOOL:
updateStatus(`LLM -> Tool (${currentTool})`);
logReActStep('action', `Calling Tool: ${currentTool}`);
flashObject(objects.llm.mesh, 1, 100);
createSignal(objects.llm, objects.tools[currentTool], SIGNAL_DURATION_TOOL, () => {
if (simulationState === SIM_STATE.LLM_TO_TOOL) { changeState(SIM_STATE.TOOL_PROCESSING); }
});
break;
case SIM_STATE.TOOL_PROCESSING:
updateStatus(`Tool (${currentTool}) Processing...`);
flashObject(objects.tools[currentTool].mesh, 2, 100);
currentTimeoutId = setTimeout(() => {
if (!currentTool || !objects.tools[currentTool]) { changeState(SIM_STATE.LLM_THINKING); } // Recover
else {
lastToolUsed = currentTool;
changeState(SIM_STATE.TOOL_TO_LLM);
}
}, THINKING_TIME);
break;
case SIM_STATE.TOOL_TO_LLM:
updateStatus(`Tool (${lastToolUsed}) -> LLM`);
logReActStep('observation', `Received observation from: ${lastToolUsed}`);
flashObject(objects.tools[lastToolUsed].mesh, 1, 100);
createSignal(objects.tools[lastToolUsed], objects.llm, SIGNAL_DURATION_TOOL, () => {
if (simulationState === SIM_STATE.TOOL_TO_LLM) {
currentStep++;
changeState(SIM_STATE.LLM_DECIDING);
}
});
currentTool = null;
break;
case SIM_STATE.LLM_DECIDING:
updateStatus(`LLM Deciding (Step ${currentStep}/${maxSteps})`);
logReActStep('thought', `Evaluating state after step ${currentStep}. Continue?`);
flashObject(objects.llm.mesh, 1, 150, 0xffff00); // Yellow flash
currentTimeoutId = setTimeout(() => {
const shouldFinish = (currentStep >= maxSteps);
if (shouldFinish) {
logReActStep('decision', `Decision: Stop tool use (>= ${maxSteps} steps).`);
updateStatus('LLM: Decision -> Final Answer');
flashObject(objects.llm.mesh, 1, 200, Colors.finalAnswer);
changeState(SIM_STATE.FINAL_ANSWER);
} else {
logReActStep('decision', `Decision: Continue tool use (< ${maxSteps} steps).`);
updateStatus('LLM: Decision -> Continue');
flashObject(objects.llm.mesh, 1, 150);
changeState(SIM_STATE.LLM_THINKING);
}
}, THINKING_TIME);
break;
case SIM_STATE.FINAL_ANSWER:
updateStatus('LLM Generating Final Answer...');
logReActStep('action', 'Generating final answer report.');
flashObject(objects.llm.mesh, 3, 100);
createFinalAnswer();
currentTimeoutId = setTimeout(() => {
logReActStep('complete', 'Analysis Complete.');
changeState(SIM_STATE.COMPLETE, "Analysis Complete.");
}, 800);
break;
case SIM_STATE.COMPLETE: case SIM_STATE.ERROR: case SIM_STATE.IDLE:
const startButton = document.getElementById('startButton'); if (startButton) startButton.disabled = false; break;
default: console.error(`Unknown simulation state: ${simulationState}`); changeState(SIM_STATE.ERROR, `Error: Unknown state ${simulationState}`);
}
}
function changeState(newState, statusMessage = null) {
console.log(`Transitioning State: ${simulationState} -> ${newState}`); simulationState = newState;
if (statusMessage) { updateStatus(statusMessage); }
if (newState !== SIM_STATE.IDLE && newState !== SIM_STATE.COMPLETE && newState !== SIM_STATE.ERROR) { setTimeout(processCurrentState, 0); }
else if (newState === SIM_STATE.COMPLETE || newState === SIM_STATE.ERROR || newState === SIM_STATE.IDLE) {
const startButton = document.getElementById('startButton'); if (startButton) startButton.disabled = false;
}
}
function resetSimulation() {
console.log("Resetting simulation..."); if (currentTimeoutId) { clearTimeout(currentTimeoutId); currentTimeoutId = null; }
resetSimulationVisuals();
if (objects.userPrompt && objects.userPrompt.body) {
const userPromptBody = objects.userPrompt.body; Matter.Body.setStatic(userPromptBody, true);
Matter.Body.setPosition(userPromptBody, { x: -75, y: -40 }); Matter.Body.setVelocity(userPromptBody, { x: 0, y: 0 });
Matter.Body.setAngularVelocity(userPromptBody, 0); Matter.Body.setStatic(userPromptBody, false);
} else { console.warn("Could not reset User Prompt position - object missing."); }
changeState(SIM_STATE.IDLE, "Simulation Reset."); currentStep = 0; currentTool = null; lastToolUsed = null;
}
function resetSimulationVisuals() {
if (currentTimeoutId) { clearTimeout(currentTimeoutId); currentTimeoutId = null; }
if(reactLogList) reactLogList.innerHTML = '';
objects.signals.forEach(signal => { if (signal.mesh) scene.remove(signal.mesh); }); objects.signals = [];
if (objects.finalAnswerObj) {
if (objects.finalAnswerObj.mesh) scene.remove(objects.finalAnswerObj.mesh);
labeledObjects = labeledObjects.filter(item => !item || item.mesh !== objects.finalAnswerObj.mesh);
const finalAnswerLabel = Array.from(labelsContainer.children).find(el => el.textContent === "Final Answer");
if (finalAnswerLabel) labelsContainer.removeChild(finalAnswerLabel);
objects.finalAnswerObj = null;
}
[objects.llm, objects.systemPrompt, objects.userPrompt, ...objects.subPrompts, ...Object.values(objects.tools)].forEach(obj => {
if (obj && obj.mesh && obj.mesh.material) { obj.mesh.material.emissive.setHex(0x000000); obj.mesh.scale.set(1, 1, 1); }
});
const subPromptMeshes = objects.subPrompts.map(sp => sp.mesh);
labeledObjects = labeledObjects.filter(item => {
if (item && item.labelElement && subPromptMeshes.includes(item.mesh)) {
if (item.labelElement.parentNode === labelsContainer) { labelsContainer.removeChild(item.labelElement); } return false;
} return true;
});
}
function createSignal(fromObj, toObj, duration, onComplete) {
if (!fromObj || !toObj || !fromObj.body || !toObj.body) { console.error("Cannot create signal: Invalid 'from' or 'to' object.", fromObj, toObj); return; }
const startPos = new THREE.Vector3(fromObj.body.position.x, fromObj.body.position.y, 5); const endPos = new THREE.Vector3(toObj.body.position.x, toObj.body.position.y, 5);
const signalGeom = new THREE.SphereGeometry(Sizes.signalRadius, 8, 8); const signalMat = new THREE.MeshBasicMaterial({ color: Colors.signal });
const signalMesh = new THREE.Mesh(signalGeom, signalMat); signalMesh.position.copy(startPos); scene.add(signalMesh);
const signal = { mesh: signalMesh, startTime: Date.now(), duration, startPos, endPos, onComplete }; objects.signals.push(signal);
}
function createFinalAnswer() {
if (objects.finalAnswerObj) return;
if (!objects.llm || !objects.llm.body || !objects.userPrompt || !objects.userPrompt.body || Object.keys(objects.tools).length === 0) {
console.error("Cannot create final answer: Core objects not ready or no tools exist.");
return;
}
const llmPos = objects.llm.body.position;
const userPromptPos = objects.userPrompt.body.position;
// Find the closest tool to the User Prompt
let closestTool = null;
let minDistSq = Infinity;
for (const toolName in objects.tools) {
const tool = objects.tools[toolName];
if (tool && tool.body) {
const dx = tool.body.position.x - userPromptPos.x;
const dy = tool.body.position.y - userPromptPos.y;
const distSq = dx * dx + dy * dy;
if (distSq < minDistSq) {
minDistSq = distSq;
closestTool = tool;
}
}
}
let answerPos;
if (!closestTool) {
console.error("Could not find closest tool to position Final Answer. Using fallback.");
answerPos = { x: llmPos.x, y: llmPos.y - Sizes.llmRadius - 40 }; // Fallback below LLM
} else {
const closestToolPos = closestTool.body.position;
// *** CALCULATE MIDPOINT ***
answerPos = {
x: (userPromptPos.x + closestToolPos.x) / 2,
y: (userPromptPos.y + closestToolPos.y) / 2
};
}
console.log("Final Answer Position:", answerPos);
const geometry = new THREE.TorusKnotGeometry(15, 5, 100, 16);
const material = new THREE.MeshStandardMaterial({ color: Colors.finalAnswer, roughness: 0.3, metalness: 0.4, emissive: Colors.finalAnswer, emissiveIntensity: 0.5 });
const mesh = new THREE.Mesh(geometry, material); mesh.position.set(answerPos.x, answerPos.y, 0);
mesh.scale.set(0.1, 0.1, 0.1); scene.add(mesh);
objects.finalAnswerObj = { mesh: mesh, startTime: Date.now() };
addLabel(mesh, "Final Answer", 20); // Normal label for final answer
const growDuration = 500; const targetScale = 1.0;
function animateGrowth() {
if (!objects.finalAnswerObj || !objects.finalAnswerObj.mesh) return;
const elapsed = Date.now() - objects.finalAnswerObj.startTime; const progress = Math.min(elapsed / growDuration, 1);
const scale = 0.1 + progress * (targetScale - 0.1);
objects.finalAnswerObj.mesh.scale.set(scale, scale, scale); objects.finalAnswerObj.mesh.rotation.y += 0.02; objects.finalAnswerObj.mesh.rotation.x += 0.01;
if (progress < 1) { requestAnimationFrame(animateGrowth); }
}
animateGrowth();
}
function flashObject(mesh, times = 1, duration = 200, flashColorHex = 0xffffff) {
if (!mesh || !mesh.material) { console.warn("Attempted to flash invalid mesh:", mesh); return; }
const originalEmissive = mesh.material.emissive.getHex(); const originalIntensity = mesh.material.emissiveIntensity; let count = 0;
function doFlash() {
if (!mesh || !mesh.material) return;
if (count >= times * 2) { mesh.material.emissive.setHex(originalEmissive); mesh.material.emissiveIntensity = originalIntensity; return; }
const isFlashOn = count % 2 === 0;
mesh.material.emissive.setHex(isFlashOn ? flashColorHex : originalEmissive);
mesh.material.emissiveIntensity = isFlashOn ? 1.0 : originalIntensity; count++;
setTimeout(doFlash, duration / 2);
}
doFlash();
}
function updateStatus(text) { if (statusElement) { statusElement.textContent = text; } }
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate); const now = Date.now();
if (engine) { Matter.Engine.update(engine, 1000 / 60); }
for (let i = 0; i < matterBodies.length; i++) {
const body = matterBodies[i]; const mesh = threeMeshes[i];
if (mesh && body && mesh.position) { mesh.position.set(body.position.x, body.position.y, mesh.position.z); mesh.rotation.z = body.angle; }
}
objects.connections.forEach(line => {
if (!line || !line.userData || !line.userData.constraint) return; const constraint = line.userData.constraint; if (!constraint.bodyA || !constraint.bodyB) return;
const posA = constraint.bodyA.position; const posB = constraint.bodyB.position;
if (line.geometry && line.geometry.attributes && line.geometry.attributes.position) {
const positions = line.geometry.attributes.position.array; positions[0] = posA.x; positions[1] = posA.y; positions[2] = 0;
positions[3] = posB.x; positions[4] = posB.y; positions[5] = 0; line.geometry.attributes.position.needsUpdate = true;
}
});
const signalsToRemove = [];
objects.signals.forEach((signal, index) => {
if (!signal || !signal.mesh) return; const elapsed = now - signal.startTime; const progress = Math.min(elapsed / signal.duration, 1);
signal.mesh.position.lerpVectors(signal.startPos, signal.endPos, progress);
if (progress >= 1) {
scene.remove(signal.mesh); signalsToRemove.push(index);
if (signal.onComplete) { try { signal.onComplete(); } catch(e) { console.error("Error in signal onComplete callback:", e); } }
}
});
for (let i = signalsToRemove.length - 1; i >= 0; i--) { objects.signals.splice(signalsToRemove[i], 1); }
if (objects.finalAnswerObj && objects.finalAnswerObj.mesh) {
const elapsed = Date.now() - objects.finalAnswerObj.startTime; if (elapsed > 500) { objects.finalAnswerObj.mesh.rotation.y += 0.01; objects.finalAnswerObj.mesh.rotation.x += 0.005; }
}
if (controls) controls.update(); if (camera && renderer) updateLabels();
if (renderer && scene && camera) { renderer.render(scene, camera); }
}
// --- Event Handlers ---
function onWindowResize() { if (camera && renderer) { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } }
function onMouseMove(event) {
if (!raycaster || !camera || !mouse || !tooltipElement) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera); const meshesToIntersect = Array.isArray(threeMeshes) ? threeMeshes : []; const intersects = raycaster.intersectObjects(meshesToIntersect);
if (intersects.length > 0) {
const intersectedMesh = intersects[0].object;
if (intersectedMesh.userData && typeof intersectedMesh.userData.info !== 'undefined') {
tooltipElement.style.display = 'block'; tooltipElement.style.left = `${event.clientX + 15}px`; tooltipElement.style.top = `${event.clientY + 15}px`; tooltipElement.textContent = intersectedMesh.userData.info;
} else { tooltipElement.style.display = 'none'; }
} else { tooltipElement.style.display = 'none'; }
}
// --- Start ---
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment