/** * 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); });