Created
March 29, 2025 18:48
Tech Writer Animation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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