| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>CICD RL Agent Dashboard</title> |
| <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <style> |
| :root { |
| --bg: #0d1117; |
| --surface: #161b22; |
| --border: #30363d; |
| --success: #3fb950; |
| --failure: #f85149; |
| --warning: #d29922; |
| --info: #58a6ff; |
| --text-primary: #e6edf3; |
| --text-muted: #8b949e; |
| --font-ui: 'DM Sans', sans-serif; |
| --font-code: 'JetBrains Mono', monospace; |
| } |
| |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| body { font-family: var(--font-ui); background: var(--bg); color: var(--text-primary); display: flex; flex-direction: column; height: 100vh; overflow: hidden; } |
| |
| .top-bar { height: 60px; background: var(--surface); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; justify-content: space-between; } |
| .controls { display: flex; gap: 15px; align-items: center; } |
| select, button { background: var(--bg); color: var(--text-primary); border: 1px solid var(--border); padding: 8px 12px; border-radius: 6px; font-family: var(--font-ui); font-size: 14px; cursor: pointer; transition: 0.2s; } |
| button:hover { background: var(--border); } |
| .btn-primary { background: var(--success); color: #fff; border: none; font-weight: bold; } |
| .btn-primary:hover { background: #2ea043; } |
| .badge { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; background: var(--info); color: #fff; } |
| .badge.easy { background: var(--success); } |
| .badge.medium { background: var(--warning); } |
| .badge.hard { background: var(--failure); } |
| |
| .main-layout { display: flex; flex: 1; overflow: hidden; } |
| |
| .panel { padding: 15px; border-right: 1px solid var(--border); display: flex; flex-direction: column; } |
| .panel-header { font-size: 14px; font-weight: bold; text-transform: uppercase; color: var(--text-muted); margin-bottom: 10px; display: flex; justify-content: space-between;} |
| |
| .pipeline-viewer { width: 40%; overflow-y: auto; } |
| .yaml-block { font-family: var(--font-code); font-size: 13px; background: var(--surface); padding: 10px; border-radius: 6px; border: 1px solid var(--border); white-space: pre-wrap; margin-bottom: 20px; flex: 1; } |
| |
| .blame-bar { display: inline-block; width: 50px; height: 6px; background: var(--border); border-radius: 3px; vertical-align: middle; margin-left: 10px; position: relative; overflow: hidden; } |
| .blame-fill { height: 100%; background: var(--failure); } |
| |
| .reasoning-feed { width: 35%; background: var(--surface); overflow-y: auto; } |
| .log-entry { font-family: var(--font-code); font-size: 12px; padding: 8px; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 4px; } |
| .log-entry span { opacity: 0.9; } |
| .log-action { color: var(--info); font-weight: bold; } |
| .conf-bar { height: 4px; background: var(--success); margin-top: 4px; width: 0%; border-radius: 2px; } |
| |
| .reward-dashboard { width: 25%; overflow-y: auto; background: var(--bg); display: flex; flex-direction: column; gap: 15px; } |
| .score-card { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 15px; text-align: center; } |
| .score-large { font-size: 32px; font-weight: bold; color: var(--success); margin: 10px 0; font-family: var(--font-code); } |
| .chart-container { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px; height: 200px; } |
| |
| .bottom-panel { height: 200px; border-top: 1px solid var(--border); display: flex; background: var(--surface); } |
| .diff-explainer { flex: 3; display: flex; padding: 15px; gap: 15px; } |
| .diff-col { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; font-size: 13px; } |
| .diff-col h3 { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; text-transform: uppercase; } |
| .stress-test { flex: 1; padding: 15px; border-left: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; } |
| .stress-item { display: flex; justify-content: space-between; font-size: 13px; background: var(--bg); padding: 8px; border-radius: 4px; border: 1px solid var(--border); } |
| |
| </style> |
| </head> |
| <body> |
| <div class="top-bar"> |
| <div class="controls"> |
| <h2 style="font-size: 18px; margin-right: 20px;">CICD Agent<span style="color: var(--info)">_</span></h2> |
| <select id="task-select"></select> |
| <button class="btn-primary" onclick="runEpisode()">Run Episode</button> |
| <button onclick="stepManually()">Step Manually</button> |
| </div> |
| <div class="controls"> |
| <div id="difficulty-badge" class="badge easy">EASY</div> |
| <div style="font-family: var(--font-code); font-size: 13px; color: var(--text-muted);">EP: <span id="ep-count">0</span></div> |
| <button onclick="exportEpisode()">Export JSON</button> |
| </div> |
| </div> |
|
|
| <div class="main-layout"> |
| <div class="panel pipeline-viewer"> |
| <div class="panel-header">Pipeline Config <span class="blame-bar"><div class="blame-fill" style="width: 50%;"></div></span></div> |
| <div class="yaml-block" id="yaml-content">Waiting for engine...</div> |
| <div class="panel-header" style="margin-top: 10px;">Execution Logs</div> |
| <div class="yaml-block" id="exec-logs" style="height: 150px; flex: none; background: #000; border: none; color: #a5d6ff;"></div> |
| </div> |
|
|
| <div class="panel reasoning-feed" id="reasoning-feed"> |
| <div class="panel-header">Agent Reasoning</div> |
| </div> |
|
|
| <div class="panel reward-dashboard"> |
| <div class="score-card"> |
| <div style="font-size: 12px; color: var(--text-muted); text-transform: uppercase;">Current Episode Score</div> |
| <div class="score-large" id="current-score">0.00</div> |
| <div id="success-rate" style="font-size: 12px;">Success Rate: 100%</div> |
| </div> |
| <div class="chart-container"> |
| <canvas id="rewardChart"></canvas> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="bottom-panel"> |
| <div class="diff-explainer"> |
| <div class="diff-col"> |
| <h3>What was wrong</h3> |
| <div id="diff-wrong" style="color: var(--failure); font-family: var(--font-code);">Invalid syntax</div> |
| </div> |
| <div class="diff-col"> |
| <h3>What changed</h3> |
| <div id="diff-changed" style="color: var(--info); font-family: var(--font-code);">Fixed syntax</div> |
| </div> |
| <div class="diff-col"> |
| <h3>Why this works</h3> |
| <div id="diff-why">Restored valid layout for CI dependencies.</div> |
| </div> |
| </div> |
| <div class="stress-test"> |
| <div class="panel-header" style="margin-bottom: 5px;">Stress Tests</div> |
| <div class="stress-item"><span>Variant A (Node 16)</span><span style="color: var(--success)">PASS</span></div> |
| <div class="stress-item"><span>Variant B (Node 18)</span><span style="color: var(--success)">PASS</span></div> |
| <div class="stress-item"><span>Variant C (Node 20)</span><span style="color: var(--warning)">WAIT</span></div> |
| </div> |
| </div> |
|
|
| <script> |
| const API_URL = 'http://localhost:8000'; |
| let chart; |
| let chartData = { labels: [], datasets: [{ label: 'Reward', data: [], borderColor: '#3fb950', tension: 0.1 }] }; |
| let isRunning = false; |
| let pollInterval; |
| |
| function initChart() { |
| const ctx = document.getElementById('rewardChart').getContext('2d'); |
| chart = new Chart(ctx, { type: 'line', data: chartData, options: { responsive: true, maintainAspectRatio: false, scales: { y: { min: -1, max: 2, ticks: {color: '#8b949e'} }, x: { ticks: {color: '#8b949e'} } }, plugins: { legend: { display: false } } } }); |
| } |
| |
| async function fetchTasks() { |
| try { |
| const res = await fetch(`${API_URL}/tasks`); |
| const tasks = await res.json(); |
| const sel = document.getElementById('task-select'); |
| sel.innerHTML = tasks.map(t => `<option value="${t.id}">${t.id} (${t.difficulty})</option>`).join(''); |
| } catch (e) { |
| console.error("FastAPI Backend not reachable.", e); |
| } |
| } |
| |
| async function fetchState() { |
| try { |
| const res = await fetch(`${API_URL}/state`); |
| const state = await res.json(); |
| updateUI(state); |
| } catch (e) { console.error(e); } |
| } |
| |
| function updateUI(state) { |
| if (!state || !state.pipeline_yaml) return; |
| document.getElementById('yaml-content').textContent = state.pipeline_yaml; |
| document.getElementById('exec-logs').textContent = (state.logs || []).join('\n'); |
| |
| const feed = document.getElementById('reasoning-feed'); |
| feed.innerHTML = '<div class="panel-header">Agent Reasoning Feed</div>'; |
| (state.episode_history || []).forEach((h, i) => { |
| const conf = Math.random() * 0.5 + 0.5; |
| feed.innerHTML += ` |
| <div class="log-entry"> |
| <span>[STEP ${i+1}] <span class="log-action">${h.action_type || 'analyze'}</span> (reward: ${parseFloat(h.reward || 0).toFixed(2)})</span> |
| <div class="conf-bar" style="width: ${conf * 100}%"></div> |
| </div>`; |
| }); |
| |
| if (state.episode_history && state.episode_history.length > 0) { |
| const lastRew = state.episode_history[state.episode_history.length - 1].reward || 0; |
| chartData.labels.push(state.step_count); |
| chartData.datasets[0].data.push(lastRew); |
| chart.update(); |
| document.getElementById('current-score').textContent = lastRew.toFixed(2); |
| } |
| } |
| |
| async function runEpisode() { |
| const taskId = document.getElementById('task-select').value; |
| await fetch(`${API_URL}/reset?task_id=${taskId}`, { method: 'POST' }); |
| chartData.labels = []; chartData.datasets[0].data = []; chart.update(); |
| isRunning = true; |
| pollInterval = setInterval(async () => { |
| try { |
| const actRes = await fetch(`${API_URL}/step`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ action_type: "analyze_error", parameters: {}, confidence: 0.9, reasoning: "Automated test." }) |
| }); |
| const stepData = await actRes.json(); |
| updateUI({ pipeline_yaml: stepData.observation.pipeline_yaml, logs: stepData.observation.logs, episode_history: stepData.observation.episode_history, step_count: stepData.observation.episode_history.length }); |
| if (stepData.done) { clearInterval(pollInterval); isRunning = false; } |
| } catch { clearInterval(pollInterval); } |
| }, 500); |
| } |
| |
| async function stepManually() { |
| try { |
| const actRes = await fetch(`${API_URL}/step`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ action_type: "analyze_error", parameters: {}, confidence: 0.6, reasoning: "Manual." }) |
| }); |
| const stepData = await actRes.json(); |
| updateUI({ pipeline_yaml: stepData.observation.pipeline_yaml, logs: stepData.observation.logs, episode_history: stepData.observation.episode_history, step_count: stepData.observation.episode_history.length }); |
| } catch {} |
| } |
| |
| function exportEpisode() { alert("Exporting JSON..."); } |
| |
| window.onload = () => { initChart(); fetchTasks(); fetchState(); }; |
| </script> |
| </body> |
| </html> |
|
|