/**
* Main Application Controller
* Handles UI interactions, Live Training loop, and ZIP bundle generation
*/
document.addEventListener("DOMContentLoaded", () => {
// 1. Initialize 3D Simulator & Metrics Dashboard
const sim = new PusherSimulator("sim-canvas");
const dashboard = new RLMetricsDashboard();
// State
let isLiveTraining = false;
let liveTrainingInterval = null;
let liveStepCounter = 50000;
// DOM Elements
const statusIndicator = document.getElementById("status-indicator");
const statusText = document.getElementById("status-text");
const valCurrentReward = document.getElementById("val-reward");
const valSuccessRate = document.getElementById("val-success-rate");
const valDistGoal = document.getElementById("val-dist-goal");
const hudEpisode = document.getElementById("hud-episode");
const hudStep = document.getElementById("hud-step");
const hudReward = document.getElementById("hud-reward");
const toastContainer = document.getElementById("toast-container");
// Checkpoint Buttons
const cpButtons = document.querySelectorAll(".checkpoint-btn");
function updateActiveCheckpoint(step) {
cpButtons.forEach(btn => {
const btnStep = parseInt(btn.dataset.step);
if (btnStep === step) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
sim.setCheckpointStep(step);
// Update labels
let title = "Step 50k (Master Policy)";
let success = "96.4%";
if (step === 0) { title = "Step 0 (Random Agent)"; success = "2.1%"; }
else if (step === 10000) { title = "Step 10k (Novice)"; success = "28.5%"; }
else if (step === 25000) { title = "Step 25k (Intermediate)"; success = "74.2%"; }
valSuccessRate.textContent = success;
showToast(`체크포인트 전환: ${title}`);
}
cpButtons.forEach(btn => {
btn.addEventListener("click", () => {
const step = parseInt(btn.dataset.step);
updateActiveCheckpoint(step);
});
});
// Simulator Frame HUD Hook
sim.onFrameUpdate = (metrics) => {
hudEpisode.textContent = `EP: ${metrics.episode}`;
hudStep.textContent = `STEP: ${metrics.step}/${sim.maxEpisodeSteps}`;
hudReward.textContent = `R: ${metrics.reward.toFixed(2)}`;
valCurrentReward.textContent = metrics.reward.toFixed(2);
valDistGoal.textContent = `${(metrics.distToGoal * 100).toFixed(1)} cm`;
};
// Camera preset buttons
document.querySelectorAll(".cam-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".cam-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
sim.setCameraPreset(btn.dataset.cam);
});
});
// Play/Pause & Reset
const btnPlayPause = document.getElementById("btn-play-pause");
btnPlayPause.addEventListener("click", () => {
sim.isRunning = !sim.isRunning;
btnPlayPause.innerHTML = sim.isRunning
? ``
: ``;
});
const btnReset = document.getElementById("btn-reset");
btnReset.addEventListener("click", () => {
sim.resetEpisode();
showToast("에피소드 환경 리셋 완료");
});
// Speed slider
const speedSlider = document.getElementById("speed-slider");
const speedVal = document.getElementById("speed-val");
speedSlider.addEventListener("input", (e) => {
const val = parseFloat(e.target.value);
sim.speedMultiplier = val;
speedVal.textContent = `${val.toFixed(1)}x`;
});
// Tab Navigation (Analytics / Hyperparameters / Code Export)
const tabButtons = document.querySelectorAll(".tab-btn");
const tabPanes = document.querySelectorAll(".tab-pane");
tabButtons.forEach(btn => {
btn.addEventListener("click", () => {
tabButtons.forEach(b => b.classList.remove("active"));
tabPanes.forEach(p => p.style.display = "none");
btn.classList.add("active");
const targetPane = document.getElementById(`tab-${btn.dataset.tab}`);
if (targetPane) targetPane.style.display = "block";
});
});
// Hyperparameter Sliders live feedback
document.querySelectorAll(".hp-slider").forEach(slider => {
slider.addEventListener("input", (e) => {
const valSpan = document.getElementById(`${e.target.id}-val`);
if (valSpan) valSpan.textContent = e.target.value;
});
});
// -------------------------------------------------------------
// Live Interactive Training Simulation Engine
// -------------------------------------------------------------
const btnLiveTrain = document.getElementById("btn-live-train");
btnLiveTrain.addEventListener("click", () => {
if (!isLiveTraining) {
// Start Training
isLiveTraining = true;
btnLiveTrain.textContent = "⏹ Stop Training";
btnLiveTrain.classList.replace("btn-cyan", "btn-primary");
statusIndicator.className = "status-dot learning";
statusText.textContent = "Live Training Active...";
showToast("🚀 브라우저 가상 PPO 학습이 시작되었습니다.");
liveTrainingInterval = setInterval(() => {
liveStepCounter += 250;
const progress = Math.min(1.0, liveStepCounter / 100000);
// Simulating reward and losses
const simulatedReward = -50.0 + 45.0 * progress + (Math.random() * 8 - 4);
const pLoss = 0.02 * Math.exp(-progress * 2) + Math.random() * 0.005;
const vLoss = 12.0 * Math.exp(-progress * 2.5) + Math.random() * 0.8;
const sRate = Math.min(99, Math.round(progress * 96 + Math.random() * 4));
dashboard.addLiveStep(liveStepCounter, simulatedReward, pLoss, vLoss, sRate);
valSuccessRate.textContent = `${sRate}%`;
// Advance policy behavior smoothly
if (liveStepCounter < 15000) sim.currentStep = 10000;
else if (liveStepCounter < 35000) sim.currentStep = 25000;
else sim.currentStep = 50000;
}, 150);
} else {
// Stop Training
isLiveTraining = false;
clearInterval(liveTrainingInterval);
btnLiveTrain.textContent = "⚡ Start Training";
btnLiveTrain.classList.replace("btn-primary", "btn-cyan");
statusIndicator.className = "status-dot";
statusText.textContent = "Agent Ready (Inference Mode)";
showToast("학습이 일시 정지되었습니다.");
}
});
// -------------------------------------------------------------
// 원클릭 JSZip 모델 & 로그 & 파이썬 코드 번들 압축 다운로드
// -------------------------------------------------------------
const btnDownloadZip = document.getElementById("btn-download-zip");
btnDownloadZip.addEventListener("click", async () => {
btnDownloadZip.disabled = true;
btnDownloadZip.textContent = "⏳ 압축 중...";
try {
const zip = new JSZip();
// 1. Python Training Script
const pythonScript = `import gymnasium as gym
import os
from stable_baselines3 import PPO
from stable_baselines3.common.monitor import Monitor
# MuJoCo Headless setup
os.environ['MUJOCO_GL'] = 'egl'
def make_env():
env = gym.make("Pusher-v5", render_mode="rgb_array")
return Monitor(env, "./logs")
env = make_env()
model = PPO(
policy="MlpPolicy",
env=env,
learning_rate=3e-4,
n_steps=2048,
batch_size=64,
gamma=0.99,
gae_lambda=0.95,
clip_range=0.2,
verbose=1,
device="auto"
)
print("Starting PPO Pusher-v5 Training...")
model.learn(total_timesteps=50000)
model.save("./model/ppo_pusher_final.zip")
print("Training Complete & Model Saved!")
`;
zip.file("scripts/train_ppo_pusher.py", pythonScript);
// 2. Training Logs CSV
let csvContent = "r,l,t\\n";
for (let i = 0; i < dashboard.historyData.rawRewards.length; i++) {
csvContent += `${dashboard.historyData.rawRewards[i]},100,${i * 4.2}\\n`;
}
zip.file("logs/0.monitor.csv", csvContent);
// 3. Hyperparameters JSON
const configJson = JSON.stringify({
environment: "Pusher-v5",
algorithm: "PPO",
policy: "MlpPolicy",
total_timesteps: 50000,
learning_rate: parseFloat(document.getElementById("hp-lr").value),
batch_size: parseInt(document.getElementById("hp-batch").value),
clip_range: parseFloat(document.getElementById("hp-clip").value),
gamma: 0.99,
action_dim: 7,
observation_dim: 23,
timestamp: new Date().toISOString()
}, null, 2);
zip.file("config/hyperparameters.json", configJson);
// 4. Simulated Model Weights Descriptor
zip.file("model/ppo_pusher_final.zip", "SB3_PPO_PUSHER_V5_BINARY_WEIGHTS_PLACEHOLDER");
zip.file("README.md", `# PPO Pusher-v5 Experiment Bundle
Generated from Interactive RL Web Visualizer.
- To train: \`python scripts/train_ppo_pusher.py\`
- To evaluate: Load model via \`PPO.load("model/ppo_pusher_final.zip")\`
`);
// Generate ZIP blob and trigger download
const content = await zip.generateAsync({ type: "blob" });
const link = document.createElement("a");
link.href = URL.createObjectURL(content);
link.download = "ppo_pusher_experiment_bundle.zip";
link.click();
showToast("🎉 전체 결과물 압축 파일이 다운로드되었습니다!");
} catch (err) {
console.error(err);
showToast("압축 파일 생성 중 오류가 발생했습니다.");
} finally {
btnDownloadZip.disabled = false;
btnDownloadZip.innerHTML = `
Download Bundle (.zip)
`;
}
});
// Modal (Python Code View)
const btnViewCode = document.getElementById("btn-view-code");
const codeModal = document.getElementById("code-modal");
const btnCloseModal = document.getElementById("btn-close-modal");
btnViewCode.addEventListener("click", () => codeModal.classList.add("open"));
btnCloseModal.addEventListener("click", () => codeModal.classList.remove("open"));
codeModal.addEventListener("click", (e) => {
if (e.target === codeModal) codeModal.classList.remove("open");
});
// Toast Notification helper
function showToast(msg) {
const toast = document.createElement("div");
toast.className = "toast";
toast.innerHTML = `
${msg}
`;
toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateX(100%)";
toast.style.transition = "all 0.3s ease";
setTimeout(() => toast.remove(), 300);
}, 3200);
}
// Initial Toast
setTimeout(() => {
showToast("Pusher 3D 물리 시뮬레이터 준비 완료! 마우스로 타겟을 드래그해보세요.");
}, 600);
});