Last active
September 12, 2025 16:46
-
-
Save boatbomber/4cd4aac61d8fac8ff87790e2fcef6ea0 to your computer and use it in GitHub Desktop.
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>Training State Dashboard</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.js"></script> | |
| <style> | |
| /* ===== RESET & BASE STYLES ===== */ | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Code", monospace; | |
| background: #0a0a0a; | |
| background-image: radial-gradient( | |
| ellipse at top left, | |
| rgba(0, 80, 120, 0.15) 0%, | |
| transparent 50% | |
| ), | |
| radial-gradient( | |
| ellipse at bottom right, | |
| rgba(120, 0, 80, 0.15) 0%, | |
| transparent 50% | |
| ); | |
| min-height: 100vh; | |
| padding: 20px; | |
| color: #e0e0e0; | |
| } | |
| /* ===== LAYOUT COMPONENTS ===== */ | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| .header { | |
| background: rgba(10, 10, 10, 0.8); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(0, 255, 255, 0.2); | |
| border-radius: 4px; | |
| padding: 30px; | |
| margin-bottom: 30px; | |
| box-shadow: 0 0 40px rgba(0, 255, 255, 0.1), | |
| inset 0 0 20px rgba(0, 255, 255, 0.02); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .header::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 1px; | |
| background: linear-gradient( | |
| 90deg, | |
| transparent 0%, | |
| rgba(0, 255, 255, 0.8) 50%, | |
| transparent 100% | |
| ); | |
| } | |
| h1 { | |
| color: #00ffff; | |
| font-size: 2.2em; | |
| margin-bottom: 20px; | |
| font-weight: 300; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| text-shadow: 0 0 20px rgba(0, 255, 255, 0.5), | |
| 0 0 40px rgba(0, 255, 255, 0.3); | |
| } | |
| .upload-section { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .file-input-wrapper { | |
| position: relative; | |
| overflow: hidden; | |
| display: inline-block; | |
| } | |
| .file-input-wrapper input[type="file"] { | |
| position: absolute; | |
| left: -9999px; | |
| } | |
| .file-input-label { | |
| display: inline-block; | |
| padding: 12px 30px; | |
| background: rgba(0, 255, 255, 0.1); | |
| color: #00ffff; | |
| border: 1px solid rgba(0, 255, 255, 0.4); | |
| border-radius: 2px; | |
| cursor: pointer; | |
| font-weight: 400; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| font-size: 0.9em; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .file-input-label:hover { | |
| background: rgba(0, 255, 255, 0.2); | |
| border-color: #00ffff; | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.4), | |
| inset 0 0 20px rgba(0, 255, 255, 0.1); | |
| transform: translateY(-1px); | |
| } | |
| .file-input-label:active { | |
| transform: translateY(0); | |
| } | |
| .file-name { | |
| color: #888; | |
| font-size: 0.9em; | |
| margin-left: 10px; | |
| font-family: "SF Mono", monospace; | |
| } | |
| .dashboard { | |
| display: none; | |
| } | |
| .dashboard.active { | |
| display: block; | |
| } | |
| /* ===== STATISTICS SECTION ===== */ | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); | |
| gap: 15px; | |
| margin-bottom: 30px; | |
| } | |
| .stat-card { | |
| background: rgba(10, 10, 10, 0.8); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(0, 255, 255, 0.1); | |
| border-radius: 2px; | |
| padding: 20px; | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.05), | |
| inset 0 0 20px rgba(0, 255, 255, 0.01); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .stat-card::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 3px; | |
| height: 100%; | |
| background: linear-gradient(180deg, transparent, #00ffff, transparent); | |
| } | |
| .stat-card:hover { | |
| border-color: rgba(0, 255, 255, 0.3); | |
| transform: translateX(2px); | |
| box-shadow: 0 0 30px rgba(0, 255, 255, 0.1), | |
| inset 0 0 30px rgba(0, 255, 255, 0.02); | |
| } | |
| .stat-label { | |
| color: #00ffff; | |
| font-size: 0.75em; | |
| margin-bottom: 8px; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| opacity: 0.7; | |
| font-weight: 300; | |
| } | |
| .stat-value { | |
| color: #ffffff; | |
| font-size: 1.8em; | |
| font-weight: 200; | |
| font-family: "SF Mono", monospace; | |
| text-shadow: 0 0 10px rgba(0, 255, 255, 0.3); | |
| } | |
| /* ===== CHART COMPONENTS ===== */ | |
| .chart-container { | |
| background: rgba(10, 10, 10, 0.8); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(0, 255, 255, 0.1); | |
| border-radius: 2px; | |
| padding: 25px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 0 30px rgba(0, 255, 255, 0.05), | |
| inset 0 0 30px rgba(0, 255, 255, 0.01); | |
| position: relative; | |
| } | |
| .chart-container::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 1px; | |
| background: linear-gradient( | |
| 90deg, | |
| transparent 0%, | |
| rgba(0, 255, 255, 0.3) 20%, | |
| rgba(0, 255, 255, 0.3) 80%, | |
| transparent 100% | |
| ); | |
| } | |
| .chart-title { | |
| color: #00ffff; | |
| font-size: 1.2em; | |
| margin-bottom: 20px; | |
| font-weight: 300; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| opacity: 0.9; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .export-btn { | |
| background: rgba(0, 255, 255, 0.1); | |
| border: 1px solid rgba(0, 255, 255, 0.3); | |
| color: #00ffff; | |
| padding: 6px 12px; | |
| border-radius: 2px; | |
| cursor: pointer; | |
| font-size: 0.7em; | |
| font-weight: 300; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| transition: all 0.2s ease; | |
| font-family: inherit; | |
| } | |
| .export-btn:hover { | |
| background: rgba(0, 255, 255, 0.2); | |
| border-color: #00ffff; | |
| box-shadow: 0 0 10px rgba(0, 255, 255, 0.3); | |
| transform: translateY(-1px); | |
| } | |
| .export-btn:active { | |
| transform: translateY(0); | |
| } | |
| .chart-wrapper { | |
| position: relative; | |
| height: 400px; | |
| background: rgba(0, 0, 0, 0.3); | |
| border: 1px solid rgba(0, 255, 255, 0.05); | |
| padding: 1px; | |
| border-radius: 2px; | |
| } | |
| /* ===== PROGRESS SECTION ===== */ | |
| .progress-container { | |
| background: rgba(10, 10, 10, 0.8); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(0, 255, 255, 0.1); | |
| border-radius: 2px; | |
| padding: 25px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 0 30px rgba(0, 255, 255, 0.05), | |
| inset 0 0 30px rgba(0, 255, 255, 0.01); | |
| position: relative; | |
| } | |
| .progress-container::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 1px; | |
| background: linear-gradient( | |
| 90deg, | |
| transparent 0%, | |
| rgba(0, 255, 255, 0.3) 20%, | |
| rgba(0, 255, 255, 0.3) 80%, | |
| transparent 100% | |
| ); | |
| } | |
| .progress-bar-wrapper { | |
| background: rgba(0, 0, 0, 0.5); | |
| border: 1px solid rgba(0, 255, 255, 0.1); | |
| border-radius: 2px; | |
| height: 32px; | |
| overflow: hidden; | |
| margin-bottom: 8px; | |
| position: relative; | |
| } | |
| .progress-bar-wrapper::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: repeating-linear-gradient( | |
| 90deg, | |
| transparent, | |
| transparent 10px, | |
| rgba(0, 255, 255, 0.03) 10px, | |
| rgba(0, 255, 255, 0.03) 20px | |
| ); | |
| pointer-events: none; | |
| } | |
| .progress-bar { | |
| background: linear-gradient( | |
| 90deg, | |
| rgba(0, 255, 255, 0.2), | |
| rgba(0, 255, 255, 0.6), | |
| rgba(0, 255, 255, 0.2) | |
| ); | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: 300; | |
| font-size: 1em; | |
| letter-spacing: 1px; | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.4), | |
| inset 0 0 20px rgba(0, 255, 255, 0.2); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .error-message { | |
| background: rgba(255, 0, 0, 0.1); | |
| border: 1px solid rgba(255, 0, 0, 0.3); | |
| color: #ff6666; | |
| padding: 15px; | |
| border-radius: 2px; | |
| margin-top: 20px; | |
| display: none; | |
| font-family: "SF Mono", monospace; | |
| font-size: 0.9em; | |
| } | |
| /* ===== RESPONSIVE LAYOUTS ===== */ | |
| .two-charts { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| } | |
| @media (max-width: 968px) { | |
| .two-charts { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .tooltip { | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 8px 12px; | |
| border-radius: 5px; | |
| font-size: 0.9em; | |
| } | |
| /* ===== LOADING & ERROR STATES ===== */ | |
| .skeleton { | |
| background-color: rgba(0, 255, 255, 0.1); | |
| border-radius: 2px; | |
| } | |
| .skeleton-text { | |
| width: 100%; | |
| height: 0.8em; | |
| margin-bottom: 0.5rem; | |
| border-radius: 2px; | |
| } | |
| .skeleton-text:last-child { | |
| width: 80%; | |
| } | |
| .skeleton-stat-value { | |
| width: 60%; | |
| height: 2em; | |
| margin-top: 8px; | |
| } | |
| .loading-message { | |
| text-align: center; | |
| color: #00ffff; | |
| font-size: 1em; | |
| margin: 20px 0; | |
| font-weight: 300; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| opacity: 0.7; | |
| } | |
| /* ===== SMOOTHING CONTROLS ===== */ | |
| .smoothing-controls { | |
| background: rgba(10, 10, 10, 0.8); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(0, 255, 255, 0.1); | |
| border-radius: 2px; | |
| padding: 25px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 0 30px rgba(0, 255, 255, 0.05), | |
| inset 0 0 30px rgba(0, 255, 255, 0.01); | |
| display: none; | |
| position: relative; | |
| } | |
| .smoothing-controls::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 1px; | |
| background: linear-gradient( | |
| 90deg, | |
| transparent 0%, | |
| rgba(0, 255, 255, 0.3) 20%, | |
| rgba(0, 255, 255, 0.3) 80%, | |
| transparent 100% | |
| ); | |
| } | |
| .smoothing-controls.active { | |
| display: block; | |
| } | |
| .smoothing-title { | |
| color: #00ffff; | |
| font-size: 1.2em; | |
| margin-bottom: 20px; | |
| font-weight: 300; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| opacity: 0.9; | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| } | |
| .slider-wrapper { | |
| flex: 1; | |
| min-width: 300px; | |
| } | |
| .slider-labels { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 12px; | |
| color: #00ffff; | |
| font-size: 0.8em; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| opacity: 0.6; | |
| } | |
| .slider { | |
| width: 100%; | |
| height: 4px; | |
| border-radius: 0; | |
| background: rgba(0, 255, 255, 0.1); | |
| outline: none; | |
| -webkit-appearance: none; | |
| cursor: pointer; | |
| position: relative; | |
| } | |
| .slider::before { | |
| content: ""; | |
| position: absolute; | |
| height: 100%; | |
| background: rgba(0, 255, 255, 0.6); | |
| width: var(--slider-fill, 50%); | |
| pointer-events: none; | |
| box-shadow: 0 0 10px rgba(0, 255, 255, 0.5); | |
| } | |
| .slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 0; | |
| background: #00ffff; | |
| border: 2px solid rgba(0, 20, 20, 0.8); | |
| cursor: pointer; | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.8), | |
| inset 0 0 5px rgba(0, 255, 255, 0.3); | |
| z-index: 2; | |
| position: relative; | |
| transform: rotate(45deg); | |
| } | |
| .slider::-webkit-slider-thumb:hover { | |
| box-shadow: 0 0 30px rgba(0, 255, 255, 1), | |
| inset 0 0 10px rgba(0, 255, 255, 0.5); | |
| } | |
| .slider::-webkit-slider-thumb:active { | |
| transform: rotate(45deg) scale(0.9); | |
| } | |
| .slider::-moz-range-thumb { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 0; | |
| background: #00ffff; | |
| border: 2px solid rgba(0, 20, 20, 0.8); | |
| cursor: pointer; | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.8), | |
| inset 0 0 5px rgba(0, 255, 255, 0.3); | |
| transform: rotate(45deg); | |
| } | |
| .slider::-moz-range-thumb:hover { | |
| box-shadow: 0 0 30px rgba(0, 255, 255, 1), | |
| inset 0 0 10px rgba(0, 255, 255, 0.5); | |
| } | |
| .slider::-moz-range-thumb:active { | |
| transform: rotate(45deg) scale(0.9); | |
| } | |
| .slider-value { | |
| background: rgba(0, 255, 255, 0.1); | |
| border: 1px solid rgba(0, 255, 255, 0.3); | |
| color: #00ffff; | |
| padding: 10px 20px; | |
| border-radius: 2px; | |
| font-weight: 300; | |
| min-width: 100px; | |
| text-align: center; | |
| font-size: 1.2em; | |
| font-family: "SF Mono", monospace; | |
| letter-spacing: 1px; | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.2), | |
| inset 0 0 10px rgba(0, 255, 255, 0.05); | |
| } | |
| .smoothing-description { | |
| color: #888; | |
| font-size: 0.8em; | |
| margin-top: 12px; | |
| text-align: center; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| opacity: 0.6; | |
| } | |
| .updating-indicator { | |
| display: inline-block; | |
| margin-left: 10px; | |
| color: #00ffff; | |
| font-size: 0.85em; | |
| opacity: 0; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .updating-indicator.active { | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>AI Training Monitor</h1> | |
| <div class="upload-section"> | |
| <div class="file-input-wrapper"> | |
| <input type="file" id="fileInput" accept=".json" /> | |
| <label for="fileInput" class="file-input-label"> | |
| Load Training State | |
| </label> | |
| </div> | |
| <span class="file-name" id="fileName">No file selected</span> | |
| </div> | |
| <div class="error-message" id="errorMessage"></div> | |
| </div> | |
| <div class="dashboard" id="dashboard"> | |
| <!-- Progress Bar --> | |
| <div class="progress-container"> | |
| <h2 class="chart-title">Training Progress</h2> | |
| <div> | |
| <div> | |
| <div class="progress-bar-wrapper"> | |
| <div class="progress-bar" id="progressBar"></div> | |
| </div> | |
| <div | |
| id="progressText" | |
| style=" | |
| color: #888; | |
| margin-top: 12px; | |
| font-size: 0.95em; | |
| text-align: center; | |
| " | |
| ></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Statistics Cards --> | |
| <div class="stats-grid" id="statsGrid"></div> | |
| <!-- Training Insights Summary --> | |
| <div | |
| class="chart-container" | |
| id="insightsContainer" | |
| style="display: none" | |
| > | |
| <h2 class="chart-title"> | |
| <span>Training Insights</span> | |
| </h2> | |
| <div | |
| id="insightsContent" | |
| style="color: #e0e0e0; line-height: 1.6" | |
| ></div> | |
| </div> | |
| <!-- Smoothing Controls --> | |
| <div class="smoothing-controls" id="smoothingControls"> | |
| <h3 class="smoothing-title">Smoothing Control</h3> | |
| <div class="slider-container"> | |
| <div class="slider-wrapper"> | |
| <div class="slider-labels"> | |
| <span>Raw Data</span> | |
| <span>Smooth Trends</span> | |
| </div> | |
| <input | |
| type="range" | |
| class="slider" | |
| id="smoothingSlider" | |
| min="0" | |
| max="1" | |
| step="0.01" | |
| value="0.5" | |
| /> | |
| <div class="smoothing-description" id="smoothingDescription"> | |
| MODERATE SMOOTHING - BALANCED | |
| </div> | |
| </div> | |
| <div class="slider-value" id="smoothingValue">50%</div> | |
| </div> | |
| <span class="updating-indicator" id="updatingIndicator" | |
| >Updating...</span | |
| > | |
| </div> | |
| <div class="two-charts"> | |
| <!-- Overall Reward Chart --> | |
| <div class="chart-container"> | |
| <h2 class="chart-title"> | |
| <span>Overall Reward</span> | |
| <button | |
| class="export-btn" | |
| onclick="ChartManager.exportChartAsPNG('reward', 'overall-reward.png')" | |
| > | |
| Export PNG | |
| </button> | |
| </h2> | |
| <div class="chart-wrapper"> | |
| <canvas id="rewardChart"></canvas> | |
| </div> | |
| </div> | |
| <!-- Individual Reward Functions Chart --> | |
| <div class="chart-container"> | |
| <h2 class="chart-title"> | |
| <span>Individual Reward Functions</span> | |
| <button | |
| class="export-btn" | |
| onclick="ChartManager.exportChartAsPNG('individualRewards', 'individual-rewards.png')" | |
| > | |
| Export PNG | |
| </button> | |
| </h2> | |
| <div class="chart-wrapper"> | |
| <canvas id="individualRewardsChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="two-charts"> | |
| <!-- Loss Chart --> | |
| <div class="chart-container"> | |
| <h2 class="chart-title"> | |
| <span>Loss Trajectory</span> | |
| <button | |
| class="export-btn" | |
| onclick="ChartManager.exportChartAsPNG('loss', 'loss-trajectory.png')" | |
| > | |
| Export PNG | |
| </button> | |
| </h2> | |
| <div class="chart-wrapper"> | |
| <canvas id="lossChart"></canvas> | |
| </div> | |
| </div> | |
| <!-- KL Divergence Chart --> | |
| <div class="chart-container"> | |
| <h2 class="chart-title"> | |
| <span>KL Divergence</span> | |
| <button | |
| class="export-btn" | |
| onclick="ChartManager.exportChartAsPNG('kl', 'kl-divergence.png')" | |
| > | |
| Export PNG | |
| </button> | |
| </h2> | |
| <div class="chart-wrapper"> | |
| <canvas id="klChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Learning Rate and Gradient Norm Charts --> | |
| <div class="two-charts"> | |
| <div class="chart-container"> | |
| <h2 class="chart-title"> | |
| <span>Learning Rate Schedule</span> | |
| <button | |
| class="export-btn" | |
| onclick="ChartManager.exportChartAsPNG('lr', 'learning-rate.png')" | |
| > | |
| Export PNG | |
| </button> | |
| </h2> | |
| <div class="chart-wrapper"> | |
| <canvas id="lrChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="chart-container"> | |
| <h2 class="chart-title"> | |
| <span>Gradient Magnitude</span> | |
| <button | |
| class="export-btn" | |
| onclick="ChartManager.exportChartAsPNG('grad', 'gradient-norm.png')" | |
| > | |
| Export PNG | |
| </button> | |
| </h2> | |
| <div class="chart-wrapper"> | |
| <canvas id="gradChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Completion Length Chart --> | |
| <div class="chart-container"> | |
| <h2 class="chart-title"> | |
| <span>Completion Length</span> | |
| <button | |
| class="export-btn" | |
| onclick="ChartManager.exportChartAsPNG('completionLength', 'completion-length.png')" | |
| > | |
| Export PNG | |
| </button> | |
| </h2> | |
| <div class="chart-wrapper"> | |
| <canvas id="completionLengthChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ===== CHART.JS CONFIGURATION ===== | |
| Chart.defaults.color = "rgba(255, 255, 255, 0.8)"; | |
| Chart.defaults.borderColor = "rgba(0, 255, 255, 0.1)"; | |
| Chart.defaults.font.family = "'SF Mono', monospace"; | |
| Chart.defaults.font.size = 12; | |
| // Plugin to add background color to charts | |
| const backgroundColorPlugin = { | |
| id: "backgroundColor", | |
| beforeDraw: (chart) => { | |
| const { ctx, width, height } = chart; | |
| ctx.save(); | |
| ctx.fillStyle = "#0a0a0a"; // Dark background matching UI | |
| ctx.fillRect(0, 0, width, height); | |
| ctx.restore(); | |
| }, | |
| }; | |
| // Register the plugin | |
| Chart.register(backgroundColorPlugin); | |
| // ===== DATA PROCESSING MODULE ===== | |
| const DataProcessor = (function () { | |
| "use strict"; | |
| // Convert smoothing level (0-1) to sigma value for Gaussian smoothing | |
| function getSmoothingSigma(smoothingLevel, dataLength) { | |
| const minSigma = 0.2; // Minimal smoothing | |
| const maxSigma = Math.max(3, dataLength / 40); // Maximum smoothing | |
| return minSigma + (maxSigma - minSigma) * smoothingLevel; | |
| } | |
| // Generate Gaussian kernel for smoothing | |
| function generateGaussianKernel(sigma) { | |
| const kernelSize = Math.ceil(sigma * 6) | 1; // Ensure odd size | |
| const kernel = new Array(kernelSize); | |
| const center = Math.floor(kernelSize / 2); | |
| let sum = 0; | |
| for (let i = 0; i < kernelSize; i++) { | |
| const x = i - center; | |
| kernel[i] = Math.exp(-(x * x) / (2 * sigma * sigma)); | |
| sum += kernel[i]; | |
| } | |
| // Normalize kernel | |
| for (let i = 0; i < kernelSize; i++) { | |
| kernel[i] /= sum; | |
| } | |
| return kernel; | |
| } | |
| // Gaussian smoothing function | |
| function gaussianSmooth(data, smoothingLevel = 0.5) { | |
| if (!data || data.length === 0) return []; | |
| if (data.length === 1) return data.slice(); | |
| const sigma = getSmoothingSigma(smoothingLevel, data.length); | |
| const kernel = generateGaussianKernel(sigma); | |
| const kernelRadius = Math.floor(kernel.length / 2); | |
| const smoothed = new Array(data.length); | |
| for (let i = 0; i < data.length; i++) { | |
| let weightedSum = 0; | |
| let totalWeight = 0; | |
| for (let j = 0; j < kernel.length; j++) { | |
| const dataIndex = i - kernelRadius + j; | |
| if (dataIndex >= 0 && dataIndex < data.length) { | |
| const value = data[dataIndex]; | |
| if (value !== null && value !== undefined && isFinite(value)) { | |
| weightedSum += value * kernel[j]; | |
| totalWeight += kernel[j]; | |
| } | |
| } | |
| } | |
| smoothed[i] = totalWeight > 0 ? weightedSum / totalWeight : data[i]; | |
| } | |
| return smoothed; | |
| } | |
| // Calculate percentile for outlier handling | |
| function calculatePercentile(arr, percentile) { | |
| const sorted = arr.filter((v) => v !== null && v !== undefined); | |
| if (sorted.length === 0) return 0; | |
| sorted.sort((a, b) => a - b); | |
| const index = Math.ceil((percentile / 100) * sorted.length) - 1; | |
| return sorted[Math.max(0, index)]; | |
| } | |
| // Get Y-axis limits that exclude outliers | |
| function getYAxisLimits( | |
| data, | |
| lowerPercentile = 1, | |
| upperPercentile = 99 | |
| ) { | |
| const validData = data.filter((v) => v !== null && v !== undefined); | |
| if (validData.length === 0) return { min: 0, max: 1 }; | |
| const lower = calculatePercentile(validData, lowerPercentile); | |
| const upper = calculatePercentile(validData, upperPercentile); | |
| const range = upper - lower; | |
| const padding = range * 0.05; | |
| return { | |
| min: Math.max(0, lower - padding), | |
| max: upper + padding, | |
| }; | |
| } | |
| // Get Y-axis limits based on smoothed data (ignores raw data outliers) | |
| function getSmoothedDataLimits(smoothedData, padding = 0.05) { | |
| const validData = smoothedData.filter( | |
| (v) => v !== null && v !== undefined && isFinite(v) | |
| ); | |
| if (validData.length === 0) return { min: undefined, max: undefined }; | |
| const min = Math.min(...validData); | |
| const max = Math.max(...validData); | |
| const range = max - min; | |
| const paddingAmount = range * padding; | |
| return { | |
| min: min - paddingAmount, | |
| max: max + paddingAmount, | |
| }; | |
| } | |
| return { | |
| gaussianSmooth, | |
| calculatePercentile, | |
| getSmoothedDataLimits, | |
| }; | |
| })(); | |
| // ===== APPLICATION STATE ===== | |
| const AppState = (function () { | |
| "use strict"; | |
| let charts = {}; | |
| let currentData = null; | |
| let smoothingLevel = 0.5; | |
| let updateTimeout = null; | |
| let isUpdating = false; | |
| return { | |
| getCharts: () => charts, | |
| setChart: (name, chart) => (charts[name] = chart), | |
| getCurrentData: () => currentData, | |
| setCurrentData: (data) => (currentData = data), | |
| getSmoothingLevel: () => smoothingLevel, | |
| setSmoothingLevel: (level) => (smoothingLevel = level), | |
| getUpdateTimeout: () => updateTimeout, | |
| setUpdateTimeout: (timeout) => (updateTimeout = timeout), | |
| isUpdating: () => isUpdating, | |
| setUpdating: (updating) => (isUpdating = updating), | |
| }; | |
| })(); | |
| // ===== UI CONTROLLER MODULE ===== | |
| const UIController = (function () { | |
| "use strict"; | |
| function showError(message) { | |
| const errorEl = document.getElementById("errorMessage"); | |
| errorEl.textContent = message; | |
| errorEl.style.display = "block"; | |
| setTimeout(() => { | |
| errorEl.style.display = "none"; | |
| }, 5000); | |
| } | |
| function showSkeletons() { | |
| const dashboard = document.getElementById("dashboard"); | |
| dashboard.style.display = "block"; | |
| // Show skeleton stats | |
| const statsGrid = document.getElementById("statsGrid"); | |
| statsGrid.innerHTML = ""; | |
| for (let i = 0; i < 4; i++) { | |
| const card = document.createElement("div"); | |
| card.className = "stat-card"; | |
| card.innerHTML = ` | |
| <div class="skeleton skeleton-text" style="width: 70%;"></div> | |
| <div class="skeleton skeleton-stat-value"></div> | |
| `; | |
| statsGrid.appendChild(card); | |
| } | |
| // Show skeleton progress bar | |
| document.getElementById("progressBar").style.width = "0%"; | |
| document.getElementById("progressBar").innerHTML = | |
| '<span style="color: rgba(255,255,255,0.7);">LOADING...</span>'; | |
| document.getElementById("progressText").innerHTML = | |
| '<span style="color: #00ffff; opacity: 0.5;">CALCULATING TRAINING PROGRESS...</span>'; | |
| // Show skeleton charts | |
| const charts = [ | |
| "lossChart", | |
| "rewardChart", | |
| "individualRewardsChart", | |
| "completionLengthChart", | |
| "klChart", | |
| "lrChart", | |
| "gradChart", | |
| ]; | |
| charts.forEach((chartId) => { | |
| const canvas = document.getElementById(chartId); | |
| const ctx = canvas.getContext("2d"); | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.fillStyle = "#999"; | |
| ctx.font = "16px sans-serif"; | |
| ctx.textAlign = "center"; | |
| ctx.fillText( | |
| "Loading chart data...", | |
| canvas.width / 2, | |
| canvas.height / 2 | |
| ); | |
| }); | |
| // Add loading message | |
| if (!document.getElementById("loadingMessage")) { | |
| const loadingMsg = document.createElement("div"); | |
| loadingMsg.id = "loadingMessage"; | |
| loadingMsg.className = "loading-message"; | |
| loadingMsg.innerHTML = "Processing Training Data..."; | |
| dashboard.insertBefore(loadingMsg, dashboard.firstChild); | |
| } | |
| } | |
| function calculateAdvancedStats(data) { | |
| if (!data.log_history || data.log_history.length === 0) return {}; | |
| const logHistory = data.log_history; | |
| const lossData = logHistory | |
| .filter((entry) => entry.loss !== undefined) | |
| .map((entry) => entry.loss); | |
| const rewardData = logHistory | |
| .filter((entry) => entry.reward !== undefined) | |
| .map((entry) => entry.reward); | |
| const gradData = logHistory | |
| .filter((entry) => entry.grad_norm !== undefined) | |
| .map((entry) => entry.grad_norm); | |
| function calculateStats(arr) { | |
| if (arr.length === 0) return null; | |
| const sorted = [...arr].sort((a, b) => a - b); | |
| const mean = arr.reduce((sum, val) => sum + val, 0) / arr.length; | |
| const variance = | |
| arr.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / | |
| arr.length; | |
| const stdDev = Math.sqrt(variance); | |
| return { | |
| mean, | |
| stdDev, | |
| min: sorted[0], | |
| max: sorted[sorted.length - 1], | |
| median: sorted[Math.floor(sorted.length / 2)], | |
| p25: sorted[Math.floor(sorted.length * 0.25)], | |
| p75: sorted[Math.floor(sorted.length * 0.75)], | |
| }; | |
| } | |
| function calculateImprovement(arr) { | |
| if (arr.length < 2) return null; | |
| const windowSize = Math.min(10, Math.floor(arr.length / 4)); | |
| if (windowSize < 1) return null; | |
| const start = | |
| arr.slice(0, windowSize).reduce((sum, val) => sum + val, 0) / | |
| windowSize; | |
| const end = | |
| arr.slice(-windowSize).reduce((sum, val) => sum + val, 0) / | |
| windowSize; | |
| if (Math.abs(start) < 1e-8) return null; // Avoid division by very small numbers | |
| return ((end - start) / Math.abs(start)) * 100; | |
| } | |
| return { | |
| loss: calculateStats(lossData), | |
| reward: calculateStats(rewardData), | |
| gradNorm: calculateStats(gradData), | |
| lossImprovement: calculateImprovement(lossData), | |
| rewardImprovement: calculateImprovement(rewardData), | |
| }; | |
| } | |
| function generateTrainingInsights(data, advancedStats) { | |
| const insights = []; | |
| // Training progress assessment (only if we have valid improvement data) | |
| if ( | |
| advancedStats.lossImprovement !== null && | |
| isFinite(advancedStats.lossImprovement) | |
| ) { | |
| const lossChange = advancedStats.lossImprovement; | |
| if (lossChange < -10) { | |
| insights.push( | |
| `<span style="color: #00ff88;">✓ Excellent loss reduction (${Math.abs( | |
| lossChange | |
| ).toFixed(1)}%)</span> - Model is learning effectively.` | |
| ); | |
| } else if (lossChange < -2) { | |
| insights.push( | |
| `<span style="color: #88ff88;">✓ Good loss reduction (${Math.abs( | |
| lossChange | |
| ).toFixed(1)}%)</span> - Training is progressing well.` | |
| ); | |
| } else if (lossChange < 2) { | |
| insights.push( | |
| `<span style="color: #ffaa00;">⚠ Loss is relatively stable</span> - Consider adjusting learning rate or checking for convergence.` | |
| ); | |
| } else { | |
| insights.push( | |
| `<span style="color: #ff6666;">⚠ Loss is increasing (${lossChange.toFixed( | |
| 1 | |
| )}%)</span> - May indicate learning rate too high or training instability.` | |
| ); | |
| } | |
| } | |
| // Gradient norm assessment | |
| if (advancedStats.gradNorm) { | |
| const currentGradNorm = | |
| data.log_history[data.log_history.length - 1]?.grad_norm; | |
| if (currentGradNorm) { | |
| if (currentGradNorm > 10) { | |
| insights.push( | |
| `<span style="color: #ff6666;">⚠ High gradient norm (${currentGradNorm.toFixed( | |
| 2 | |
| )})</span> - Consider gradient clipping to stabilize training.` | |
| ); | |
| } else if (currentGradNorm < 0.001) { | |
| insights.push( | |
| `<span style="color: #ffaa00;">⚠ Very low gradient norm (${currentGradNorm.toFixed( | |
| 4 | |
| )})</span> - May indicate vanishing gradients or convergence.` | |
| ); | |
| } else { | |
| insights.push( | |
| `<span style="color: #00ff88;">✓ Healthy gradient norm (${currentGradNorm.toFixed( | |
| 3 | |
| )})</span> - Gradients are in good range for stable learning.` | |
| ); | |
| } | |
| } | |
| } | |
| // Reward improvement assessment | |
| if ( | |
| advancedStats.rewardImprovement !== null && | |
| isFinite(advancedStats.rewardImprovement) | |
| ) { | |
| const rewardChange = advancedStats.rewardImprovement; | |
| if (rewardChange > 10) { | |
| insights.push( | |
| `<span style="color: #00ff88;">✓ Excellent reward improvement (${rewardChange.toFixed( | |
| 1 | |
| )}%)</span> - Model performance is increasing significantly.` | |
| ); | |
| } else if (rewardChange > 2) { | |
| insights.push( | |
| `<span style="color: #88ff88;">✓ Good reward improvement (${rewardChange.toFixed( | |
| 1 | |
| )}%)</span> - Model is improving steadily.` | |
| ); | |
| } else if (rewardChange > -2) { | |
| insights.push( | |
| `<span style="color: #ffaa00;">⚠ Reward is relatively stable</span> - Model may be reaching plateau.` | |
| ); | |
| } | |
| } | |
| // Training stability assessment (only if we have enough data points) | |
| if ( | |
| advancedStats.loss && | |
| advancedStats.loss.stdDev && | |
| data.log_history.length > 20 | |
| ) { | |
| const cv = | |
| advancedStats.loss.stdDev / Math.abs(advancedStats.loss.mean); | |
| if (cv < 0.05) { | |
| insights.push( | |
| `<span style="color: #00ff88;">✓ Very stable training</span> - Consistent loss reduction with low variance.` | |
| ); | |
| } else if (cv > 0.3) { | |
| insights.push( | |
| `<span style="color: #ffaa00;">⚠ Training shows some instability</span> - Loss variance is ${( | |
| cv * 100 | |
| ).toFixed(1)}%. Consider learning rate scheduling.` | |
| ); | |
| } | |
| } | |
| // Training duration insight | |
| if (data.log_history && data.log_history.length > 0) { | |
| const totalEntries = data.log_history.length; | |
| insights.push( | |
| `<span style="color: #88aaff;">ℹ Training log contains ${totalEntries.toLocaleString()} data points</span> - ${ | |
| totalEntries > 1000 | |
| ? "Rich dataset for analysis" | |
| : "Consider longer training for better insights" | |
| }.` | |
| ); | |
| } | |
| return insights; | |
| } | |
| function displayStats(data) { | |
| const statsGrid = document.getElementById("statsGrid"); | |
| statsGrid.innerHTML = ""; | |
| const advancedStats = calculateAdvancedStats(data); | |
| const stats = []; | |
| // Only show Change metrics: Loss Change, Reward Change, Grad Norm Change, and Change for each reward function | |
| if (data.log_history && data.log_history.length > 0) { | |
| // Loss Change | |
| if ( | |
| advancedStats.lossImprovement !== null && | |
| isFinite(advancedStats.lossImprovement) | |
| ) { | |
| const improvement = advancedStats.lossImprovement; | |
| const color = | |
| improvement < 0 | |
| ? 'style="color: #00ff88;"' | |
| : 'style="color: #ff6666;"'; | |
| stats.push({ | |
| label: "Loss Change", | |
| value: `<span ${color}>${improvement.toFixed(1)}%</span>`, | |
| }); | |
| } | |
| // Reward Change | |
| if ( | |
| advancedStats.rewardImprovement !== null && | |
| isFinite(advancedStats.rewardImprovement) | |
| ) { | |
| const improvement = advancedStats.rewardImprovement; | |
| const color = | |
| improvement > 0 | |
| ? 'style="color: #00ff88;"' | |
| : 'style="color: #ff6666;"'; | |
| stats.push({ | |
| label: "Reward Change", | |
| value: `<span ${color}>${improvement.toFixed(1)}%</span>`, | |
| }); | |
| } | |
| // Grad Norm Change | |
| if (advancedStats.gradNorm) { | |
| const gradNormData = data.log_history | |
| .filter((entry) => entry.grad_norm !== undefined) | |
| .map((entry) => entry.grad_norm); | |
| if (gradNormData.length > 10) { | |
| const windowSize = Math.min( | |
| 10, | |
| Math.floor(gradNormData.length / 4) | |
| ); | |
| const start = | |
| gradNormData | |
| .slice(0, windowSize) | |
| .reduce((sum, val) => sum + val, 0) / windowSize; | |
| const end = | |
| gradNormData | |
| .slice(-windowSize) | |
| .reduce((sum, val) => sum + val, 0) / windowSize; | |
| if (Math.abs(start) > 1e-10) { | |
| const gradNormChange = | |
| ((end - start) / Math.abs(start)) * 100; | |
| const color = | |
| Math.abs(gradNormChange) < 10 | |
| ? 'style="color: #00ff88;"' | |
| : 'style="color: #ffaa00;"'; | |
| stats.push({ | |
| label: "Grad Norm Change", | |
| value: `<span ${color}>${gradNormChange.toFixed( | |
| 1 | |
| )}%</span>`, | |
| }); | |
| } | |
| } | |
| } | |
| // KL Change | |
| const klData = data.log_history | |
| .filter((entry) => entry.kl !== undefined) | |
| .map((entry) => entry.kl); | |
| if (klData.length > 10) { | |
| const windowSize = Math.min(10, Math.floor(klData.length / 4)); | |
| const start = | |
| klData.slice(0, windowSize).reduce((sum, val) => sum + val, 0) / | |
| windowSize; | |
| const end = | |
| klData.slice(-windowSize).reduce((sum, val) => sum + val, 0) / | |
| windowSize; | |
| if (Math.abs(start) > 1e-10) { | |
| const klChange = ((end - start) / Math.abs(start)) * 100; | |
| const color = | |
| Math.abs(klChange) < 20 | |
| ? 'style="color: #00ff88;"' | |
| : 'style="color: #ffaa00;"'; | |
| stats.push({ | |
| label: "KL Change", | |
| value: `<span ${color}>${klChange.toFixed(1)}%</span>`, | |
| }); | |
| } | |
| } | |
| } | |
| // Individual reward function Change stats only | |
| const rewardKeys = ChartManager.extractRewardFunctions( | |
| data.log_history || [] | |
| ); | |
| rewardKeys.forEach((rewardKey) => { | |
| const rewardName = ChartManager.getRewardDisplayName(rewardKey); | |
| const rewardData = data.log_history | |
| .filter((entry) => entry[rewardKey] !== undefined) | |
| .map((entry) => entry[rewardKey]); | |
| if (rewardData.length > 10) { | |
| // Only show Change (improvement) | |
| const windowSize = Math.min( | |
| 10, | |
| Math.floor(rewardData.length / 4) | |
| ); | |
| const start = | |
| rewardData | |
| .slice(0, windowSize) | |
| .reduce((sum, val) => sum + val, 0) / windowSize; | |
| const end = | |
| rewardData | |
| .slice(-windowSize) | |
| .reduce((sum, val) => sum + val, 0) / windowSize; | |
| if (Math.abs(start) > 1e-10) { | |
| const rewardChange = ((end - start) / Math.abs(start)) * 100; | |
| const color = | |
| rewardChange > 0 | |
| ? 'style="color: #00ff88;"' | |
| : 'style="color: #ff6666;"'; | |
| stats.push({ | |
| label: `${rewardName} Change`, | |
| value: `<span ${color}>${rewardChange.toFixed(1)}%</span>`, | |
| }); | |
| } | |
| } | |
| }); | |
| stats.forEach((stat) => { | |
| const card = document.createElement("div"); | |
| card.className = "stat-card"; | |
| card.innerHTML = ` | |
| <div class="stat-label">${stat.label}</div> | |
| <div class="stat-value">${stat.value}</div> | |
| `; | |
| statsGrid.appendChild(card); | |
| }); | |
| // Display insights | |
| const insights = generateTrainingInsights(data, advancedStats); | |
| if (insights.length > 0) { | |
| const insightsContainer = | |
| document.getElementById("insightsContainer"); | |
| const insightsContent = document.getElementById("insightsContent"); | |
| insightsContainer.style.display = "block"; | |
| insightsContent.innerHTML = | |
| '<ul style="margin: 0; padding-left: 20px;">' + | |
| insights | |
| .map( | |
| (insight) => `<li style="margin-bottom: 8px;">${insight}</li>` | |
| ) | |
| .join("") + | |
| "</ul>"; | |
| } | |
| } | |
| function displayProgress(data) { | |
| const progress = (data.global_step / data.max_steps) * 100; | |
| const progressBar = document.getElementById("progressBar"); | |
| progressBar.style.width = progress + "%"; | |
| progressBar.textContent = progress.toFixed(1) + "%"; | |
| const epochInfo = `Epoch ${data.epoch?.toFixed(4) || 0} of ${ | |
| data.num_train_epochs || 0 | |
| }`; | |
| const stepInfo = `Step ${ | |
| data.global_step?.toLocaleString() || 0 | |
| } of ${data.max_steps?.toLocaleString() || 0}`; | |
| document.getElementById( | |
| "progressText" | |
| ).innerHTML = `<span style="color: #00ffff;">${epochInfo}</span> <span style="color: #444; margin: 0 10px;">|</span> <span style="color: #00ffff;">${stepInfo}</span>`; | |
| } | |
| function updateSmoothingUI(smoothingLevel) { | |
| const percentage = Math.round(smoothingLevel * 100); | |
| document.getElementById( | |
| "smoothingValue" | |
| ).textContent = `${percentage}%`; | |
| const slider = document.getElementById("smoothingSlider"); | |
| slider.style.setProperty("--slider-fill", `${smoothingLevel * 100}%`); | |
| const descElement = document.getElementById("smoothingDescription"); | |
| if (smoothingLevel === 0) { | |
| descElement.textContent = "RAW DATA - NO SMOOTHING"; | |
| } else if (smoothingLevel < 0.3) { | |
| descElement.textContent = "LIGHT SMOOTHING - HIGH DETAIL"; | |
| } else if (smoothingLevel < 0.7) { | |
| descElement.textContent = "MODERATE SMOOTHING - BALANCED"; | |
| } else { | |
| descElement.textContent = "HEAVY SMOOTHING - TREND FOCUS"; | |
| } | |
| } | |
| return { | |
| showError, | |
| showSkeletons, | |
| displayStats, | |
| displayProgress, | |
| updateSmoothingUI, | |
| }; | |
| })(); | |
| // ===== EVENT HANDLERS ===== | |
| document | |
| .getElementById("fileInput") | |
| .addEventListener("change", function (e) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| document.getElementById("fileName").textContent = file.name; | |
| UIController.showSkeletons(); | |
| const reader = new FileReader(); | |
| reader.onload = function (e) { | |
| try { | |
| const data = JSON.parse(e.target.result); | |
| setTimeout(() => Dashboard.displayDashboard(data), 10); | |
| } catch (error) { | |
| UIController.showError( | |
| "Error parsing JSON file: " + error.message | |
| ); | |
| document.getElementById("dashboard").style.display = "none"; | |
| } | |
| }; | |
| reader.readAsText(file); | |
| } | |
| }); | |
| // Smoothing control event listener with debouncing | |
| const smoothingSlider = document.getElementById("smoothingSlider"); | |
| if (smoothingSlider) { | |
| smoothingSlider.addEventListener("input", function (e) { | |
| const smoothingLevel = parseFloat(e.target.value); | |
| AppState.setSmoothingLevel(smoothingLevel); | |
| UIController.updateSmoothingUI(smoothingLevel); | |
| // Debounce the chart update | |
| if (AppState.getUpdateTimeout()) { | |
| clearTimeout(AppState.getUpdateTimeout()); | |
| } | |
| const indicator = document.getElementById("updatingIndicator"); | |
| if (indicator) { | |
| indicator.textContent = "PENDING..."; | |
| indicator.classList.add("active"); | |
| } | |
| const timeout = setTimeout(() => { | |
| if (AppState.getCurrentData() && !AppState.isUpdating()) { | |
| if (indicator) indicator.textContent = "UPDATING..."; | |
| Dashboard.updateAllChartsAsync(); | |
| } | |
| }, 300); | |
| AppState.setUpdateTimeout(timeout); | |
| }); | |
| smoothingSlider.style.setProperty("--slider-fill", "50%"); | |
| } | |
| // ===== CHART MANAGER MODULE ===== | |
| const ChartManager = (function () { | |
| "use strict"; | |
| // Common chart options | |
| function getBaseChartOptions() { | |
| return { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: false, | |
| scales: { | |
| x: { | |
| display: true, | |
| title: { | |
| display: true, | |
| text: "STEP", | |
| color: "rgba(255, 255, 255, 0.8)", | |
| }, | |
| grid: { color: "rgba(0, 255, 255, 0.1)", drawBorder: false }, | |
| ticks: { color: "rgba(255, 255, 255, 0.7)" }, | |
| }, | |
| y: { | |
| display: true, | |
| grid: { color: "rgba(0, 255, 255, 0.1)", drawBorder: false }, | |
| ticks: { color: "rgba(255, 255, 255, 0.7)" }, | |
| }, | |
| }, | |
| layout: { | |
| padding: 0, | |
| }, | |
| // Enable the background color plugin | |
| plugins: { | |
| backgroundColor: true, | |
| legend: { | |
| display: true, | |
| position: "top", | |
| labels: { | |
| color: "rgba(255, 255, 255, 0.8)", | |
| font: { family: "'SF Mono', monospace", size: 11 }, | |
| }, | |
| }, | |
| tooltip: { | |
| mode: "index", | |
| intersect: false, | |
| backgroundColor: "rgba(0, 0, 0, 0.9)", | |
| borderColor: "rgba(0, 255, 255, 0.3)", | |
| borderWidth: 1, | |
| titleColor: "#00ffff", | |
| bodyColor: "rgba(255, 255, 255, 0.8)", | |
| }, | |
| }, | |
| }; | |
| } | |
| function createDataset(label, data, color, isSmoothed = false) { | |
| const alpha = isSmoothed ? 0.8 : 0.2; | |
| const bgAlpha = isSmoothed ? 0.1 : 0.05; | |
| const width = isSmoothed ? 2 : 1; | |
| return { | |
| label, | |
| data, | |
| borderColor: color.replace("0.8)", `${alpha})`), | |
| backgroundColor: color.replace("0.8)", `${bgAlpha})`), | |
| borderWidth: width, | |
| pointRadius: 0, | |
| pointHoverRadius: isSmoothed ? 4 : 3, | |
| tension: 0.1, | |
| }; | |
| } | |
| // Helper functions for data availability | |
| function hasRewardData(logHistory) { | |
| return logHistory.some( | |
| (entry) => entry.reward !== undefined && entry.reward !== null | |
| ); | |
| } | |
| function hasIndividualRewardData(logHistory) { | |
| return extractRewardFunctions(logHistory).length > 0; | |
| } | |
| function hasCompletionLengthData(logHistory) { | |
| return logHistory.some( | |
| (entry) => | |
| entry["completions/mean_length"] !== undefined && | |
| entry["completions/mean_length"] !== null | |
| ); | |
| } | |
| function hasKLData(logHistory) { | |
| return logHistory.some( | |
| (entry) => entry.kl !== undefined && entry.kl !== null | |
| ); | |
| } | |
| function extractRewardFunctions(logHistory) { | |
| const rewardKeys = new Set(); | |
| const sampleSize = Math.min(10, logHistory.length); | |
| for (let i = 0; i < sampleSize; i++) { | |
| const entry = logHistory[i]; | |
| if (!entry) continue; | |
| for (const key in entry) { | |
| if (key.startsWith("rewards/") && key.endsWith("/mean")) { | |
| rewardKeys.add(key); | |
| } | |
| } | |
| } | |
| return Array.from(rewardKeys); | |
| } | |
| function getRewardDisplayName(key) { | |
| const name = key.replace("rewards/", "").replace("/mean", ""); | |
| return name.charAt(0).toUpperCase() + name.slice(1); | |
| } | |
| function getRewardColor(index) { | |
| const colors = [ | |
| "rgba(255, 99, 132, 0.8)", | |
| "rgba(54, 162, 235, 0.8)", | |
| "rgba(255, 206, 86, 0.8)", | |
| "rgba(75, 192, 192, 0.8)", | |
| "rgba(153, 102, 255, 0.8)", | |
| "rgba(255, 159, 64, 0.8)", | |
| "rgba(199, 199, 199, 0.8)", | |
| "rgba(83, 102, 255, 0.8)", | |
| "rgba(255, 99, 255, 0.8)", | |
| "rgba(99, 255, 132, 0.8)", | |
| ]; | |
| return colors[index % colors.length]; | |
| } | |
| function exportChartAsPNG(chartName, customFilename) { | |
| const chart = AppState.getCharts()[chartName]; | |
| if (!chart) { | |
| console.error(`Chart '${chartName}' not found`); | |
| return; | |
| } | |
| try { | |
| // Charts now have built-in background color, so we can export directly | |
| const dataURL = chart.toBase64Image("image/png", 1.0); | |
| // Create download link | |
| const link = document.createElement("a"); | |
| link.download = customFilename || `${chartName}-chart.png`; | |
| link.href = dataURL; | |
| // Trigger download | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| } catch (error) { | |
| console.error("Export failed:", error); | |
| alert("Failed to export chart. Please try again."); | |
| } | |
| } | |
| return { | |
| getBaseChartOptions, | |
| createDataset, | |
| hasRewardData, | |
| hasIndividualRewardData, | |
| hasCompletionLengthData, | |
| hasKLData, | |
| extractRewardFunctions, | |
| getRewardDisplayName, | |
| getRewardColor, | |
| exportChartAsPNG, | |
| }; | |
| })(); | |
| // ===== MAIN DASHBOARD MODULE ===== | |
| const Dashboard = (function () { | |
| "use strict"; | |
| async function displayDashboard(data) { | |
| AppState.setCurrentData(data); | |
| // Remove loading message | |
| const loadingMsg = document.getElementById("loadingMessage"); | |
| if (loadingMsg) loadingMsg.remove(); | |
| // Show smoothing controls and set initial display | |
| const smoothingLevel = AppState.getSmoothingLevel(); | |
| document.getElementById("smoothingControls").classList.add("active"); | |
| document.getElementById("smoothingSlider").value = smoothingLevel; | |
| UIController.updateSmoothingUI(smoothingLevel); | |
| // Display stats and progress | |
| UIController.displayStats(data); | |
| UIController.displayProgress(data); | |
| // Configure chart visibility and create charts | |
| configureChartVisibility(data); | |
| requestAnimationFrame(() => { | |
| createAllCharts(data.log_history); | |
| // Force resize/redraw of all charts | |
| setTimeout(() => { | |
| Object.values(AppState.getCharts()).forEach((chart) => { | |
| if (chart && typeof chart.resize === "function") { | |
| chart.resize(); | |
| } | |
| }); | |
| }, 100); | |
| }); | |
| } | |
| function createAllCharts(logHistory) { | |
| createLossChart(logHistory); | |
| createRewardChart(logHistory); | |
| createIndividualRewardsChart(logHistory); | |
| createCompletionLengthChart(logHistory); | |
| createKLChart(logHistory); | |
| createLearningRateChart(logHistory); | |
| createGradientNormChart(logHistory); | |
| } | |
| async function updateAllChartsAsync() { | |
| const currentData = AppState.getCurrentData(); | |
| if (!currentData || !currentData.log_history || AppState.isUpdating()) | |
| return; | |
| AppState.setUpdating(true); | |
| const indicator = document.getElementById("updatingIndicator"); | |
| if (indicator) indicator.classList.add("active"); | |
| const chartFlags = configureChartVisibility(currentData); | |
| // Update charts with breaks for UI responsiveness | |
| const charts = [ | |
| () => createLossChart(currentData.log_history, true), | |
| () => | |
| chartFlags.showRewardChart && | |
| createRewardChart(currentData.log_history, true), | |
| () => | |
| chartFlags.showIndividualRewards && | |
| createIndividualRewardsChart(currentData.log_history, true), | |
| () => | |
| chartFlags.showCompletionLength && | |
| createCompletionLengthChart(currentData.log_history, true), | |
| () => | |
| chartFlags.showKLChart && | |
| createKLChart(currentData.log_history, true), | |
| () => createGradientNormChart(currentData.log_history, true), | |
| () => createLearningRateChart(currentData.log_history, true), | |
| ]; | |
| for (const chartFn of charts) { | |
| await new Promise((resolve) => { | |
| requestAnimationFrame(() => { | |
| chartFn(); | |
| resolve(); | |
| }); | |
| }); | |
| await new Promise((resolve) => setTimeout(resolve, 10)); | |
| } | |
| if (indicator) { | |
| setTimeout(() => indicator.classList.remove("active"), 200); | |
| } | |
| AppState.setUpdating(false); | |
| } | |
| return { | |
| displayDashboard, | |
| updateAllChartsAsync, | |
| }; | |
| })(); | |
| // ===== CHART CREATION FUNCTIONS ===== | |
| function createLossChart(logHistory) { | |
| const existingChart = AppState.getCharts().loss; | |
| if (existingChart) existingChart.destroy(); | |
| const ctx = document.getElementById("lossChart").getContext("2d"); | |
| const lossData = logHistory.filter((entry) => entry.loss !== undefined); | |
| const rawLosses = lossData.map((entry) => entry.loss); | |
| const smoothedLosses = DataProcessor.gaussianSmooth( | |
| rawLosses, | |
| AppState.getSmoothingLevel() | |
| ); | |
| const options = ChartManager.getBaseChartOptions(); | |
| options.scales.y.title = { | |
| display: true, | |
| text: "LOSS", | |
| color: "rgba(255, 255, 255, 0.8)", | |
| }; | |
| // Scale based on smoothed data to avoid raw data outliers | |
| const yLimits = DataProcessor.getSmoothedDataLimits(smoothedLosses); | |
| if (yLimits.min !== undefined && yLimits.max !== undefined) { | |
| options.scales.y.min = yLimits.min; | |
| options.scales.y.max = yLimits.max; | |
| } | |
| options.plugins.tooltip.callbacks = { | |
| label: (context) => | |
| `${context.dataset.label}: ${context.parsed.y.toFixed(4)}`, | |
| }; | |
| const chart = new Chart(ctx, { | |
| type: "line", | |
| data: { | |
| labels: lossData.map((entry) => entry.step), | |
| datasets: [ | |
| ChartManager.createDataset( | |
| "Training Loss (Raw)", | |
| rawLosses, | |
| "rgba(0, 255, 255, 0.8)", | |
| false | |
| ), | |
| ChartManager.createDataset( | |
| "Training Loss (Smoothed)", | |
| smoothedLosses, | |
| "rgba(0, 255, 255, 0.8)", | |
| true | |
| ), | |
| ], | |
| }, | |
| options, | |
| }); | |
| AppState.setChart("loss", chart); | |
| } | |
| function createRewardChart(logHistory) { | |
| const existingChart = AppState.getCharts().reward; | |
| if (existingChart) existingChart.destroy(); | |
| const ctx = document.getElementById("rewardChart").getContext("2d"); | |
| const rewardData = logHistory.filter( | |
| (entry) => entry.reward !== undefined | |
| ); | |
| const rawRewards = rewardData.map((entry) => entry.reward); | |
| const smoothedRewards = DataProcessor.gaussianSmooth( | |
| rawRewards, | |
| AppState.getSmoothingLevel() | |
| ); | |
| const options = ChartManager.getBaseChartOptions(); | |
| options.scales.y.title = { | |
| display: true, | |
| text: "REWARD", | |
| color: "rgba(255, 255, 255, 0.8)", | |
| }; | |
| // Scale based on smoothed data to avoid raw data outliers | |
| const yLimits = DataProcessor.getSmoothedDataLimits(smoothedRewards); | |
| if (yLimits.min !== undefined && yLimits.max !== undefined) { | |
| options.scales.y.min = yLimits.min; | |
| options.scales.y.max = yLimits.max; | |
| } | |
| options.plugins.tooltip.callbacks = { | |
| label: (context) => | |
| `${context.dataset.label}: ${context.parsed.y.toFixed(4)}`, | |
| }; | |
| const chart = new Chart(ctx, { | |
| type: "line", | |
| data: { | |
| labels: rewardData.map((entry) => entry.step), | |
| datasets: [ | |
| ChartManager.createDataset( | |
| "Overall Reward (Raw)", | |
| rawRewards, | |
| "rgba(0, 255, 128, 0.8)", | |
| false | |
| ), | |
| ChartManager.createDataset( | |
| "Overall Reward (Smoothed)", | |
| smoothedRewards, | |
| "rgba(0, 255, 128, 0.8)", | |
| true | |
| ), | |
| ], | |
| }, | |
| options, | |
| }); | |
| AppState.setChart("reward", chart); | |
| } | |
| // Function to show/hide chart containers based on data availability | |
| function configureChartVisibility(data) { | |
| const logHistory = data.log_history || []; | |
| const showRewardChart = ChartManager.hasRewardData(logHistory); | |
| const showIndividualRewards = | |
| ChartManager.hasIndividualRewardData(logHistory); | |
| const showCompletionLength = | |
| ChartManager.hasCompletionLengthData(logHistory); | |
| const showKLChart = ChartManager.hasKLData(logHistory); | |
| // Show/hide chart containers | |
| const containers = [ | |
| { selector: "#rewardChart", show: showRewardChart }, | |
| { selector: "#individualRewardsChart", show: showIndividualRewards }, | |
| { selector: "#completionLengthChart", show: showCompletionLength }, | |
| { selector: "#klChart", show: showKLChart }, | |
| ]; | |
| containers.forEach(({ selector, show }) => { | |
| const container = document | |
| .querySelector(selector) | |
| ?.closest(".chart-container"); | |
| if (container) container.style.display = show ? "block" : "none"; | |
| }); | |
| return { | |
| showRewardChart, | |
| showIndividualRewards, | |
| showCompletionLength, | |
| showKLChart, | |
| }; | |
| } | |
| function createIndividualRewardsChart(logHistory) { | |
| const existingChart = AppState.getCharts().individualRewards; | |
| if (existingChart) existingChart.destroy(); | |
| const ctx = document | |
| .getElementById("individualRewardsChart") | |
| .getContext("2d"); | |
| const rewardKeys = ChartManager.extractRewardFunctions(logHistory); | |
| if (rewardKeys.length === 0) { | |
| ctx.fillStyle = "#999"; | |
| ctx.font = "16px sans-serif"; | |
| ctx.textAlign = "center"; | |
| ctx.fillText( | |
| "No reward functions found", | |
| ctx.canvas.width / 2, | |
| ctx.canvas.height / 2 | |
| ); | |
| return; | |
| } | |
| const rewardData = logHistory.filter((entry) => { | |
| return rewardKeys.some((key) => entry[key] !== undefined); | |
| }); | |
| if (rewardData.length === 0) { | |
| ctx.fillStyle = "#999"; | |
| ctx.font = "16px sans-serif"; | |
| ctx.textAlign = "center"; | |
| ctx.fillText( | |
| "No reward data found", | |
| ctx.canvas.width / 2, | |
| ctx.canvas.height / 2 | |
| ); | |
| return; | |
| } | |
| // Collect all smoothed data for scaling calculation | |
| const allSmoothedData = []; | |
| const datasets = rewardKeys.flatMap((rewardKey, index) => { | |
| const rawRewardData = rewardData.map((entry) => entry[rewardKey]); | |
| const smoothedRewardData = DataProcessor.gaussianSmooth( | |
| rawRewardData, | |
| AppState.getSmoothingLevel() | |
| ); | |
| const color = ChartManager.getRewardColor(index); | |
| // Add smoothed data to collection for scaling | |
| allSmoothedData.push( | |
| ...smoothedRewardData.filter( | |
| (v) => v !== null && v !== undefined && isFinite(v) | |
| ) | |
| ); | |
| return [ | |
| ChartManager.createDataset( | |
| ChartManager.getRewardDisplayName(rewardKey) + " (Raw)", | |
| rawRewardData, | |
| color, | |
| false | |
| ), | |
| ChartManager.createDataset( | |
| ChartManager.getRewardDisplayName(rewardKey) + " (Smoothed)", | |
| smoothedRewardData, | |
| color, | |
| true | |
| ), | |
| ]; | |
| }); | |
| const options = ChartManager.getBaseChartOptions(); | |
| options.scales.y.title = { | |
| display: true, | |
| text: "INDIVIDUAL REWARD", | |
| color: "rgba(255, 255, 255, 0.8)", | |
| }; | |
| // Scale based on all smoothed reward data to avoid raw data outliers | |
| if (allSmoothedData.length > 0) { | |
| const min = Math.min(...allSmoothedData); | |
| const max = Math.max(...allSmoothedData); | |
| const range = max - min; | |
| const padding = range * 0.05; | |
| options.scales.y.min = min - padding; | |
| options.scales.y.max = max + padding; | |
| } | |
| options.plugins.tooltip.callbacks = { | |
| label: (context) => { | |
| const label = context.dataset.label || ""; | |
| const value = context.parsed.y; | |
| return value === null || value === undefined | |
| ? `${label}: N/A` | |
| : `${label}: ${value.toFixed(4)}`; | |
| }, | |
| }; | |
| const chart = new Chart(ctx, { | |
| type: "line", | |
| data: { | |
| labels: rewardData.map((entry) => entry.step), | |
| datasets, | |
| }, | |
| options, | |
| }); | |
| AppState.setChart("individualRewards", chart); | |
| } | |
| function createCompletionLengthChart(logHistory) { | |
| const existingChart = AppState.getCharts().completionLength; | |
| if (existingChart) existingChart.destroy(); | |
| const ctx = document | |
| .getElementById("completionLengthChart") | |
| .getContext("2d"); | |
| const completionData = logHistory.filter( | |
| (entry) => entry["completions/mean_length"] !== undefined | |
| ); | |
| const rawLengths = completionData.map( | |
| (entry) => entry["completions/mean_length"] | |
| ); | |
| const smoothedLengths = DataProcessor.gaussianSmooth( | |
| rawLengths, | |
| AppState.getSmoothingLevel() | |
| ); | |
| const options = ChartManager.getBaseChartOptions(); | |
| options.scales.y.title = { | |
| display: true, | |
| text: "COMPLETION LENGTH", | |
| color: "rgba(255, 255, 255, 0.8)", | |
| }; | |
| // Scale based on smoothed data to avoid raw data outliers | |
| const yLimits = DataProcessor.getSmoothedDataLimits(smoothedLengths); | |
| if (yLimits.min !== undefined && yLimits.max !== undefined) { | |
| options.scales.y.min = yLimits.min; | |
| options.scales.y.max = yLimits.max; | |
| } | |
| options.plugins.tooltip.callbacks = { | |
| label: (context) => | |
| `${context.dataset.label}: ${Math.round(context.parsed.y)} tokens`, | |
| }; | |
| const chart = new Chart(ctx, { | |
| type: "line", | |
| data: { | |
| labels: completionData.map((entry) => entry.step), | |
| datasets: [ | |
| ChartManager.createDataset( | |
| "Completion Length (Raw)", | |
| rawLengths, | |
| "rgba(153, 102, 255, 0.8)", | |
| false | |
| ), | |
| ChartManager.createDataset( | |
| "Completion Length (Smoothed)", | |
| smoothedLengths, | |
| "rgba(153, 102, 255, 0.8)", | |
| true | |
| ), | |
| ], | |
| }, | |
| options, | |
| }); | |
| AppState.setChart("completionLength", chart); | |
| } | |
| function createKLChart(logHistory) { | |
| const existingChart = AppState.getCharts().kl; | |
| if (existingChart) existingChart.destroy(); | |
| const ctx = document.getElementById("klChart").getContext("2d"); | |
| const klData = logHistory.filter((entry) => entry.kl !== undefined); | |
| const rawKLs = klData.map((entry) => entry.kl); | |
| const smoothedKLs = DataProcessor.gaussianSmooth( | |
| rawKLs, | |
| AppState.getSmoothingLevel() | |
| ); | |
| const options = ChartManager.getBaseChartOptions(); | |
| options.scales.y.title = { | |
| display: true, | |
| text: "KL DIVERGENCE", | |
| color: "rgba(255, 255, 255, 0.8)", | |
| }; | |
| // Scale based on smoothed data to avoid raw data outliers | |
| const yLimits = DataProcessor.getSmoothedDataLimits(smoothedKLs); | |
| if (yLimits.min !== undefined && yLimits.max !== undefined) { | |
| options.scales.y.min = yLimits.min; | |
| options.scales.y.max = yLimits.max; | |
| } | |
| options.plugins.tooltip.callbacks = { | |
| label: (context) => | |
| `${context.dataset.label}: ${context.parsed.y.toFixed(4)}`, | |
| }; | |
| const chart = new Chart(ctx, { | |
| type: "line", | |
| data: { | |
| labels: klData.map((entry) => entry.step), | |
| datasets: [ | |
| ChartManager.createDataset( | |
| "KL Divergence (Raw)", | |
| rawKLs, | |
| "rgba(255, 165, 0, 0.8)", | |
| false | |
| ), | |
| ChartManager.createDataset( | |
| "KL Divergence (Smoothed)", | |
| smoothedKLs, | |
| "rgba(255, 165, 0, 0.8)", | |
| true | |
| ), | |
| ], | |
| }, | |
| options, | |
| }); | |
| AppState.setChart("kl", chart); | |
| } | |
| function createLearningRateChart(logHistory) { | |
| const existingChart = AppState.getCharts().lr; | |
| if (existingChart) existingChart.destroy(); | |
| const ctx = document.getElementById("lrChart").getContext("2d"); | |
| const lrData = logHistory.filter( | |
| (entry) => entry.learning_rate !== undefined | |
| ); | |
| const options = ChartManager.getBaseChartOptions(); | |
| options.scales.y.title = { | |
| display: true, | |
| text: "LEARNING RATE", | |
| color: "rgba(255, 255, 255, 0.8)", | |
| }; | |
| options.scales.y.type = "logarithmic"; | |
| options.scales.y.ticks = { | |
| color: "rgba(255, 255, 255, 0.7)", | |
| callback: function (value) { | |
| if (value === 0) return "0"; | |
| if (value < 0.0001) return value.toExponential(0); | |
| if (value >= 0.01) return value.toFixed(2); | |
| if (value >= 0.001) return value.toFixed(3); | |
| return value.toFixed(4); | |
| }, | |
| maxTicksLimit: 8, | |
| autoSkip: true, | |
| autoSkipPadding: 10, | |
| }; | |
| options.plugins.tooltip.callbacks = { | |
| label: (context) => { | |
| const value = context.parsed.y; | |
| if (value === 0) return "LR: 0"; | |
| if (value >= 0.01) return `LR: ${value.toFixed(4)}`; | |
| if (value >= 0.0001) return `LR: ${value.toFixed(6)}`; | |
| return `LR: ${value.toExponential(2)}`; | |
| }, | |
| }; | |
| const chart = new Chart(ctx, { | |
| type: "line", | |
| data: { | |
| labels: lrData.map((entry) => entry.step), | |
| datasets: [ | |
| ChartManager.createDataset( | |
| "Learning Rate", | |
| lrData.map((entry) => entry.learning_rate), | |
| "rgba(255, 200, 0, 0.8)", | |
| true | |
| ), | |
| ], | |
| }, | |
| options, | |
| }); | |
| AppState.setChart("lr", chart); | |
| } | |
| function createGradientNormChart(logHistory) { | |
| const existingChart = AppState.getCharts().grad; | |
| if (existingChart) existingChart.destroy(); | |
| const ctx = document.getElementById("gradChart").getContext("2d"); | |
| const gradData = logHistory.filter( | |
| (entry) => entry.grad_norm !== undefined | |
| ); | |
| const rawGradNorms = gradData.map((entry) => entry.grad_norm); | |
| const smoothedGradNorms = DataProcessor.gaussianSmooth( | |
| rawGradNorms, | |
| AppState.getSmoothingLevel() | |
| ); | |
| const options = ChartManager.getBaseChartOptions(); | |
| options.scales.y.title = { | |
| display: true, | |
| text: "GRADIENT NORM", | |
| color: "rgba(255, 255, 255, 0.8)", | |
| }; | |
| // Scale based on smoothed data to avoid raw data outliers | |
| const yLimits = DataProcessor.getSmoothedDataLimits(smoothedGradNorms); | |
| if (yLimits.min !== undefined && yLimits.max !== undefined) { | |
| options.scales.y.min = yLimits.min; | |
| options.scales.y.max = yLimits.max; | |
| } | |
| options.plugins.tooltip.callbacks = { | |
| label: (context) => | |
| `${context.dataset.label}: ${context.parsed.y.toFixed(4)}`, | |
| }; | |
| const chart = new Chart(ctx, { | |
| type: "line", | |
| data: { | |
| labels: gradData.map((entry) => entry.step), | |
| datasets: [ | |
| ChartManager.createDataset( | |
| "Gradient Norm (Raw)", | |
| rawGradNorms, | |
| "rgba(255, 0, 128, 0.8)", | |
| false | |
| ), | |
| ChartManager.createDataset( | |
| "Gradient Norm (Smoothed)", | |
| smoothedGradNorms, | |
| "rgba(255, 0, 128, 0.8)", | |
| true | |
| ), | |
| ], | |
| }, | |
| options, | |
| }); | |
| AppState.setChart("grad", chart); | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment