| <!doctype html>
|
| <html lang="es">
|
| <head>
|
| <meta charset="UTF-8" />
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| <title>PAMPAr Classroom</title>
|
| <style>
|
| :root {
|
| --bg: #0d1117;
|
| --bg2: #161b22;
|
| --bg3: #21262d;
|
| --border: #30363d;
|
| --text: #c9d1d9;
|
| --text2: #8b949e;
|
| --green: #3fb950;
|
| --red: #f85149;
|
| --blue: #58a6ff;
|
| --yellow: #d29922;
|
| --purple: #bc8cff;
|
| --orange: #f0883e;
|
| }
|
|
|
| * {
|
| margin: 0;
|
| padding: 0;
|
| box-sizing: border-box;
|
| }
|
|
|
| body {
|
| font-family: "Cascadia Code", "Fira Code", "Consolas", monospace;
|
| background: var(--bg);
|
| color: var(--text);
|
| height: 100vh;
|
| overflow: hidden;
|
| }
|
|
|
|
|
| .app {
|
| display: grid;
|
| grid-template-columns: 1fr 280px;
|
| grid-template-rows: 48px 1fr 40px;
|
| height: 100vh;
|
| gap: 1px;
|
| background: var(--border);
|
| }
|
|
|
|
|
| .header {
|
| grid-column: 1 / -1;
|
| background: var(--bg2);
|
| display: flex;
|
| align-items: center;
|
| padding: 0 16px;
|
| gap: 16px;
|
| }
|
|
|
| .header h1 {
|
| font-size: 14px;
|
| font-weight: 600;
|
| color: var(--blue);
|
| }
|
|
|
| .header .controls {
|
| display: flex;
|
| gap: 8px;
|
| margin-left: auto;
|
| }
|
|
|
| .header .rec-indicator {
|
| display: flex;
|
| align-items: center;
|
| gap: 6px;
|
| font-size: 11px;
|
| color: var(--red);
|
| }
|
|
|
| .header .rec-dot {
|
| width: 8px;
|
| height: 8px;
|
| border-radius: 50%;
|
| background: var(--red);
|
| animation: blink 1s infinite;
|
| }
|
|
|
| @keyframes blink {
|
| 50% {
|
| opacity: 0.3;
|
| }
|
| }
|
|
|
| .btn {
|
| padding: 4px 12px;
|
| border: 1px solid var(--border);
|
| border-radius: 4px;
|
| background: var(--bg3);
|
| color: var(--text);
|
| font-family: inherit;
|
| font-size: 12px;
|
| cursor: pointer;
|
| transition: background 0.15s;
|
| }
|
|
|
| .btn:hover {
|
| background: var(--border);
|
| }
|
| .btn.primary {
|
| background: #238636;
|
| border-color: #2ea043;
|
| }
|
| .btn.primary:hover {
|
| background: #2ea043;
|
| }
|
| .btn.danger {
|
| background: #da3633;
|
| border-color: #f85149;
|
| }
|
| .btn.danger:hover {
|
| background: #f85149;
|
| }
|
|
|
|
|
| .chat {
|
| background: var(--bg);
|
| overflow-y: auto;
|
| padding: 12px;
|
| scroll-behavior: smooth;
|
| }
|
|
|
| .chat::-webkit-scrollbar {
|
| width: 6px;
|
| }
|
| .chat::-webkit-scrollbar-track {
|
| background: var(--bg);
|
| }
|
| .chat::-webkit-scrollbar-thumb {
|
| background: var(--bg3);
|
| border-radius: 3px;
|
| }
|
|
|
| .msg {
|
| margin-bottom: 12px;
|
| padding: 8px 12px;
|
| border-radius: 6px;
|
| font-size: 12px;
|
| line-height: 1.5;
|
| animation: fadeIn 0.3s;
|
| max-width: 90%;
|
| }
|
|
|
| @keyframes fadeIn {
|
| from {
|
| opacity: 0;
|
| transform: translateY(4px);
|
| }
|
| to {
|
| opacity: 1;
|
| transform: translateY(0);
|
| }
|
| }
|
|
|
| .msg.system {
|
| background: var(--bg2);
|
| border-left: 3px solid var(--blue);
|
| color: var(--text2);
|
| max-width: 100%;
|
| font-size: 11px;
|
| }
|
|
|
| .msg.problem {
|
| background: var(--bg2);
|
| border-left: 3px solid var(--yellow);
|
| }
|
|
|
| .msg.problem .level-badge {
|
| display: inline-block;
|
| padding: 1px 6px;
|
| border-radius: 3px;
|
| font-size: 10px;
|
| font-weight: 600;
|
| margin-bottom: 6px;
|
| }
|
|
|
| .msg.problem .level-badge.l1 {
|
| background: #1a7f37;
|
| color: #aff5b4;
|
| }
|
| .msg.problem .level-badge.l2 {
|
| background: #0550ae;
|
| color: #a5d6ff;
|
| }
|
| .msg.problem .level-badge.l3 {
|
| background: #6e40c9;
|
| color: #d2a8ff;
|
| }
|
| .msg.problem .level-badge.l4 {
|
| background: #bf4b8a;
|
| color: #ffadda;
|
| }
|
| .msg.problem .level-badge.l5 {
|
| background: #b35900;
|
| color: #ffd8b5;
|
| }
|
|
|
| .msg.student {
|
| background: #1a1f2e;
|
| border-left: 3px solid var(--purple);
|
| margin-left: 20px;
|
| }
|
|
|
| .msg.teacher {
|
| background: #1a2e1a;
|
| border-left: 3px solid var(--green);
|
| margin-left: 20px;
|
| }
|
|
|
| .msg.teacher.wrong {
|
| background: #2e1a1a;
|
| border-left: 3px solid var(--red);
|
| }
|
|
|
| .msg.solution {
|
| background: #1a2e2e;
|
| border-left: 3px solid var(--blue);
|
| margin-left: 40px;
|
| }
|
|
|
| .msg.metrics {
|
| background: var(--bg2);
|
| border: 1px solid var(--border);
|
| max-width: 100%;
|
| display: grid;
|
| grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
| gap: 8px;
|
| padding: 8px 12px;
|
| }
|
|
|
| .metric {
|
| text-align: center;
|
| }
|
|
|
| .metric .value {
|
| font-size: 16px;
|
| font-weight: 700;
|
| }
|
|
|
| .metric .label {
|
| font-size: 10px;
|
| color: var(--text2);
|
| text-transform: uppercase;
|
| }
|
|
|
| .msg .role {
|
| font-weight: 600;
|
| font-size: 11px;
|
| margin-bottom: 4px;
|
| }
|
|
|
| .msg .role.student-role {
|
| color: var(--purple);
|
| }
|
| .msg .role.teacher-role {
|
| color: var(--green);
|
| }
|
|
|
| .msg pre {
|
| background: var(--bg);
|
| padding: 6px 8px;
|
| border-radius: 4px;
|
| overflow-x: auto;
|
| margin-top: 6px;
|
| font-size: 11px;
|
| white-space: pre-wrap;
|
| }
|
|
|
| .msg.levelup {
|
| background: linear-gradient(135deg, #1a2e1a 0%, #1a1f2e 100%);
|
| border: 1px solid var(--green);
|
| text-align: center;
|
| max-width: 100%;
|
| font-size: 14px;
|
| padding: 12px;
|
| }
|
|
|
|
|
| .sidebar {
|
| background: var(--bg2);
|
| padding: 12px;
|
| overflow-y: auto;
|
| font-size: 11px;
|
| }
|
|
|
| .sidebar h2 {
|
| font-size: 11px;
|
| text-transform: uppercase;
|
| color: var(--text2);
|
| margin-bottom: 8px;
|
| letter-spacing: 1px;
|
| }
|
|
|
| .sidebar .section {
|
| margin-bottom: 16px;
|
| }
|
|
|
| .stat-row {
|
| display: flex;
|
| justify-content: space-between;
|
| padding: 3px 0;
|
| border-bottom: 1px solid var(--bg);
|
| }
|
|
|
| .stat-row .stat-label {
|
| color: var(--text2);
|
| }
|
| .stat-row .stat-value {
|
| font-weight: 600;
|
| }
|
|
|
|
|
| .progress-bar {
|
| height: 4px;
|
| background: var(--bg);
|
| border-radius: 2px;
|
| margin: 6px 0;
|
| overflow: hidden;
|
| }
|
|
|
| .progress-bar .fill {
|
| height: 100%;
|
| border-radius: 2px;
|
| transition: width 0.5s;
|
| }
|
|
|
| .fill.green {
|
| background: var(--green);
|
| }
|
| .fill.blue {
|
| background: var(--blue);
|
| }
|
| .fill.yellow {
|
| background: var(--yellow);
|
| }
|
| .fill.red {
|
| background: var(--red);
|
| }
|
| .fill.purple {
|
| background: var(--purple);
|
| }
|
|
|
|
|
| .level-display {
|
| display: flex;
|
| gap: 4px;
|
| margin: 8px 0;
|
| }
|
|
|
| .level-pip {
|
| flex: 1;
|
| height: 6px;
|
| border-radius: 3px;
|
| background: var(--bg);
|
| transition: background 0.3s;
|
| }
|
|
|
| .level-pip.active {
|
| background: var(--green);
|
| }
|
| .level-pip.current {
|
| background: var(--yellow);
|
| box-shadow: 0 0 4px var(--yellow);
|
| }
|
|
|
|
|
| .chart-canvas {
|
| width: 100%;
|
| height: 60px;
|
| margin: 4px 0;
|
| }
|
|
|
|
|
| .footer {
|
| grid-column: 1 / -1;
|
| background: var(--bg2);
|
| display: flex;
|
| align-items: center;
|
| padding: 0 16px;
|
| font-size: 11px;
|
| color: var(--text2);
|
| gap: 16px;
|
| }
|
|
|
| .status-dot {
|
| width: 8px;
|
| height: 8px;
|
| border-radius: 50%;
|
| background: var(--text2);
|
| }
|
|
|
| .status-dot.connected {
|
| background: var(--green);
|
| }
|
| .status-dot.running {
|
| background: var(--green);
|
| animation: blink 1s infinite;
|
| }
|
| .status-dot.stopped {
|
| background: var(--red);
|
| }
|
|
|
|
|
| .thinking {
|
| display: inline-flex;
|
| gap: 3px;
|
| margin-left: 8px;
|
| }
|
|
|
| .thinking span {
|
| width: 4px;
|
| height: 4px;
|
| border-radius: 50%;
|
| background: var(--text2);
|
| animation: bounce 1.2s infinite;
|
| }
|
|
|
| .thinking span:nth-child(2) {
|
| animation-delay: 0.2s;
|
| }
|
| .thinking span:nth-child(3) {
|
| animation-delay: 0.4s;
|
| }
|
|
|
| @keyframes bounce {
|
| 0%,
|
| 80%,
|
| 100% {
|
| transform: translateY(0);
|
| }
|
| 40% {
|
| transform: translateY(-6px);
|
| }
|
| }
|
| </style>
|
| </head>
|
| <body>
|
| <div class="app">
|
|
|
| <div class="header">
|
| <h1>π« PAMPAr Classroom</h1>
|
| <span id="sessionInfo" style="font-size: 11px; color: var(--text2)"
|
| >Conectando...</span
|
| >
|
| <div id="recIndicator" class="rec-indicator" style="display: none">
|
| <div class="rec-dot"></div>
|
| REC
|
| </div>
|
| <div class="controls">
|
| <button class="btn primary" id="btnStart" onclick="startLessons()">
|
| βΆ Iniciar
|
| </button>
|
| <button
|
| class="btn danger"
|
| id="btnStop"
|
| onclick="stopLessons()"
|
| style="display: none"
|
| >
|
| βΉ Detener
|
| </button>
|
| <button class="btn" id="btnSave" onclick="saveSesion()">
|
| πΎ Guardar
|
| </button>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="chat" id="chat"></div>
|
|
|
|
|
| <div class="sidebar">
|
| <div class="section">
|
| <h2>Nivel actual</h2>
|
| <div
|
| style="font-size: 20px; font-weight: 700; color: var(--yellow)"
|
| id="levelNum"
|
| >
|
| 1
|
| </div>
|
| <div id="levelName" style="color: var(--text2)">Fundamentos</div>
|
| <div class="level-display" id="levelPips">
|
| <div class="level-pip current"></div>
|
| <div class="level-pip"></div>
|
| <div class="level-pip"></div>
|
| <div class="level-pip"></div>
|
| <div class="level-pip"></div>
|
| </div>
|
| </div>
|
|
|
| <div class="section">
|
| <h2>MΓ©tricas</h2>
|
| <div class="stat-row">
|
| <span class="stat-label">Lecciones</span>
|
| <span class="stat-value" id="statLessons">0</span>
|
| </div>
|
| <div class="stat-row">
|
| <span class="stat-label">Accuracy</span>
|
| <span
|
| class="stat-value"
|
| id="statAccuracy"
|
| style="color: var(--green)"
|
| >0%</span
|
| >
|
| </div>
|
| <div class="stat-row">
|
| <span class="stat-label">Loss</span>
|
| <span class="stat-value" id="statLoss">β</span>
|
| </div>
|
| <div class="stat-row">
|
| <span class="stat-label">EWC Penalty</span>
|
| <span class="stat-value" id="statEwc">β</span>
|
| </div>
|
| <div class="stat-row">
|
| <span class="stat-label">Brain Score</span>
|
| <span class="stat-value" id="statBrain" style="color: var(--blue)"
|
| >β</span
|
| >
|
| </div>
|
| <div class="stat-row">
|
| <span class="stat-label">Replay Buffer</span>
|
| <span class="stat-value" id="statReplay">0</span>
|
| </div>
|
| </div>
|
|
|
| <div class="section">
|
| <h2>Accuracy</h2>
|
| <div class="progress-bar">
|
| <div class="fill green" id="accBar" style="width: 0%"></div>
|
| </div>
|
| </div>
|
|
|
| <div class="section">
|
| <h2>Brain Score</h2>
|
| <div class="progress-bar">
|
| <div class="fill blue" id="brainBar" style="width: 0%"></div>
|
| </div>
|
| </div>
|
|
|
| <div class="section">
|
| <h2>Loss (ΓΊltimas 20)</h2>
|
| <canvas class="chart-canvas" id="lossChart"></canvas>
|
| </div>
|
|
|
| <div class="section">
|
| <h2>Brain (ΓΊltimos 20)</h2>
|
| <canvas class="chart-canvas" id="brainChart"></canvas>
|
| </div>
|
|
|
| <div class="section">
|
| <h2>Correctas/Incorrectas</h2>
|
| <canvas
|
| class="chart-canvas"
|
| id="correctChart"
|
| style="height: 30px"
|
| ></canvas>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="footer">
|
| <div class="status-dot" id="statusDot"></div>
|
| <span id="statusText">Desconectado</span>
|
| <span style="margin-left: auto" id="elapsedTime">00:00:00</span>
|
| </div>
|
| </div>
|
|
|
| <script>
|
|
|
| const state = {
|
| running: false,
|
| connected: false,
|
| startTime: null,
|
| lessons: 0,
|
| correct: 0,
|
| level: 1,
|
| lossHistory: [],
|
| brainHistory: [],
|
| correctHistory: [],
|
| recording: true,
|
| };
|
|
|
| const chat = document.getElementById("chat");
|
| let eventSource = null;
|
| let timerInterval = null;
|
|
|
|
|
| function connectSSE() {
|
| if (eventSource) eventSource.close();
|
| eventSource = new EventSource("/events");
|
|
|
| eventSource.onopen = () => {
|
| state.connected = true;
|
| updateStatus("connected", "Conectado");
|
| document.getElementById("sessionInfo").textContent = "Listo";
|
| };
|
|
|
| eventSource.onerror = () => {
|
| state.connected = false;
|
| updateStatus("stopped", "Desconectado");
|
| };
|
|
|
|
|
| const handlers = {
|
| system: (d) => addMsg("system", d),
|
| lesson_start: (d) => {
|
| const data = JSON.parse(d);
|
| addMsg(
|
| "problem",
|
| `<span class="level-badge l${data.level}">Nivel ${data.level} β ${data.level_name}</span>` +
|
| `<div style="margin-top:4px">LecciΓ³n ${data.lesson_id}</div>` +
|
| `<div style="margin-top:6px;color:var(--text)">${escHtml(data.problem)}</div>`,
|
| );
|
| },
|
| student_thinking: () => {
|
| addMsg(
|
| "student",
|
| '<div class="role student-role">π§βπ Alumno pensando<span class="thinking"><span></span><span></span><span></span></span></div>',
|
| "thinking-msg",
|
| );
|
| },
|
| student_answer: (d) => {
|
| removeMsg("thinking-msg");
|
| const data = JSON.parse(d);
|
| addMsg(
|
| "student",
|
| '<div class="role student-role">π§βπ Alumno</div>' +
|
| `<pre>${escHtml(data.answer)}</pre>`,
|
| );
|
| },
|
| teacher_evaluating: () => {
|
| addMsg(
|
| "teacher",
|
| '<div class="role teacher-role">π¨βπ« Profesor evaluando<span class="thinking"><span></span><span></span><span></span></span></div>',
|
| "eval-msg",
|
| );
|
| },
|
| teacher_feedback: (d) => {
|
| removeMsg("eval-msg");
|
| const data = JSON.parse(d);
|
| const cls = data.correct ? "teacher" : "teacher wrong";
|
| const icon = data.correct ? "β
" : "β";
|
| addMsg(
|
| cls,
|
| `<div class="role teacher-role">π¨βπ« Profesor ${icon}</div>` +
|
| `<div style="margin-top:4px">${escHtml(data.feedback)}</div>`,
|
| );
|
| },
|
| teacher_solution: (d) => {
|
| const data = JSON.parse(d);
|
| addMsg(
|
| "solution",
|
| '<div style="font-size:10px;color:var(--blue);margin-bottom:4px">π SoluciΓ³n correcta:</div>' +
|
| `<pre>${escHtml(data.solution)}</pre>`,
|
| );
|
| },
|
| training: (d) => {
|
| const data = JSON.parse(d);
|
| addMsg(
|
| "system",
|
| `β‘ Entrenando con ${data.batch_size} ejemplo(s)...`,
|
| );
|
| },
|
| lesson_complete: (d) => {
|
| const data = JSON.parse(d);
|
| state.lessons = data.lesson_id;
|
| state.level = data.level;
|
| state.lossHistory.push(data.loss);
|
| state.brainHistory.push(data.brain_score);
|
| state.correctHistory.push(data.correct);
|
| if (data.correct) state.correct++;
|
|
|
| if (state.lossHistory.length > 50) state.lossHistory.shift();
|
| if (state.brainHistory.length > 50) state.brainHistory.shift();
|
|
|
| updateSidebar(data);
|
| drawCharts();
|
|
|
| addMsg(
|
| "metrics",
|
| `<div class="metric"><div class="value" style="color:${data.correct ? "var(--green)" : "var(--red)"}">${data.correct ? "β
" : "β"}</div><div class="label">Resultado</div></div>` +
|
| `<div class="metric"><div class="value">${(data.accuracy * 100).toFixed(0)}%</div><div class="label">Accuracy</div></div>` +
|
| `<div class="metric"><div class="value">${data.loss.toFixed(3)}</div><div class="label">Loss</div></div>` +
|
| `<div class="metric"><div class="value">${(data.brain_score * 100).toFixed(0)}%</div><div class="label">Brain</div></div>`,
|
| );
|
| },
|
| level_up: (d) => {
|
| const data = JSON.parse(d);
|
| addMsg(
|
| "levelup",
|
| `π Β‘NIVEL UP!<br>Nivel ${data.new_level}: ${data.nombre}<br>` +
|
| `<span style="font-size:11px;color:var(--text2)">Accuracy: ${(data.accuracy * 100).toFixed(0)}%</span>`,
|
| );
|
| updateLevel(data.new_level);
|
| },
|
| checkpoint: (d) => {
|
| const data = JSON.parse(d);
|
| addMsg("system", `πΎ Checkpoint guardado β lecciΓ³n ${data.lesson}`);
|
| },
|
| session_saved: (d) => {
|
| const data = JSON.parse(d);
|
| addMsg("system", `π SesiΓ³n guardada: ${data.lessons} lecciones`);
|
| },
|
| error: (d) => addMsg("system", `β ${d}`),
|
| };
|
|
|
| for (const [event, handler] of Object.entries(handlers)) {
|
| eventSource.addEventListener(event, (e) => handler(e.data));
|
| }
|
| }
|
|
|
|
|
| function addMsg(cls, html, id) {
|
| const div = document.createElement("div");
|
| div.className = `msg ${cls}`;
|
| div.innerHTML = html;
|
| if (id) div.id = id;
|
| chat.appendChild(div);
|
| chat.scrollTop = chat.scrollHeight;
|
| }
|
|
|
| function removeMsg(id) {
|
| const el = document.getElementById(id);
|
| if (el) el.remove();
|
| }
|
|
|
| function escHtml(s) {
|
| if (!s) return "";
|
| return s
|
| .replace(/&/g, "&")
|
| .replace(/</g, "<")
|
| .replace(/>/g, ">");
|
| }
|
|
|
|
|
| async function startLessons() {
|
| try {
|
| await fetch("/start", { method: "POST" });
|
| state.running = true;
|
| state.startTime = Date.now();
|
| document.getElementById("btnStart").style.display = "none";
|
| document.getElementById("btnStop").style.display = "";
|
| document.getElementById("recIndicator").style.display = "flex";
|
| updateStatus("running", "Entrenando");
|
| startTimer();
|
| } catch (e) {
|
| addMsg("system", `Error: ${e.message}`);
|
| }
|
| }
|
|
|
| async function stopLessons() {
|
| try {
|
| await fetch("/stop", { method: "POST" });
|
| state.running = false;
|
| document.getElementById("btnStart").style.display = "";
|
| document.getElementById("btnStop").style.display = "none";
|
| document.getElementById("recIndicator").style.display = "none";
|
| updateStatus("connected", "Detenido");
|
| stopTimer();
|
| } catch (e) {
|
| addMsg("system", `Error: ${e.message}`);
|
| }
|
| }
|
|
|
| async function saveSesion() {
|
| try {
|
| const resp = await fetch("/save", { method: "POST" });
|
| const data = await resp.json();
|
| addMsg("system", `πΎ Guardado: ${data.path}`);
|
| } catch (e) {
|
| addMsg("system", `Error: ${e.message}`);
|
| }
|
| }
|
|
|
|
|
| function startTimer() {
|
| timerInterval = setInterval(() => {
|
| if (!state.startTime) return;
|
| const elapsed = Math.floor((Date.now() - state.startTime) / 1000);
|
| const h = String(Math.floor(elapsed / 3600)).padStart(2, "0");
|
| const m = String(Math.floor((elapsed % 3600) / 60)).padStart(2, "0");
|
| const s = String(elapsed % 60).padStart(2, "0");
|
| document.getElementById("elapsedTime").textContent = `${h}:${m}:${s}`;
|
| }, 1000);
|
| }
|
|
|
| function stopTimer() {
|
| if (timerInterval) clearInterval(timerInterval);
|
| }
|
|
|
|
|
| function updateStatus(cls, text) {
|
| const dot = document.getElementById("statusDot");
|
| dot.className = `status-dot ${cls}`;
|
| document.getElementById("statusText").textContent = text;
|
| }
|
|
|
|
|
| function updateSidebar(data) {
|
| document.getElementById("statLessons").textContent = data.lesson_id;
|
| document.getElementById("statAccuracy").textContent =
|
| (data.accuracy * 100).toFixed(1) + "%";
|
| document.getElementById("statLoss").textContent = data.loss.toFixed(4);
|
| document.getElementById("statEwc").textContent =
|
| data.ewc_penalty.toFixed(6);
|
| document.getElementById("statBrain").textContent =
|
| (data.brain_score * 100).toFixed(1) + "%";
|
| document.getElementById("statReplay").textContent = data.replay_size;
|
|
|
| document.getElementById("accBar").style.width =
|
| data.accuracy * 100 + "%";
|
| document.getElementById("brainBar").style.width =
|
| data.brain_score * 100 + "%";
|
|
|
| updateLevel(data.level);
|
| }
|
|
|
| function updateLevel(level) {
|
| document.getElementById("levelNum").textContent = level;
|
| const names = {
|
| 1: "Fundamentos",
|
| 2: "Estructuras de control",
|
| 3: "Funciones avanzadas",
|
| 4: "Clases y OOP",
|
| 5: "Patrones avanzados",
|
| };
|
| document.getElementById("levelName").textContent = names[level] || "";
|
|
|
| const pips = document.getElementById("levelPips").children;
|
| for (let i = 0; i < 5; i++) {
|
| pips[i].className = "level-pip";
|
| if (i < level - 1) pips[i].classList.add("active");
|
| if (i === level - 1) pips[i].classList.add("current");
|
| }
|
| }
|
|
|
|
|
| function drawCharts() {
|
| drawLineChart(
|
| "lossChart",
|
| state.lossHistory.slice(-20),
|
| "var(--yellow)",
|
| );
|
| drawLineChart(
|
| "brainChart",
|
| state.brainHistory.slice(-20),
|
| "var(--blue)",
|
| );
|
| drawCorrectChart();
|
| }
|
|
|
| function drawLineChart(canvasId, data, color) {
|
| const canvas = document.getElementById(canvasId);
|
| if (!canvas || data.length < 2) return;
|
|
|
| const ctx = canvas.getContext("2d");
|
| const dpr = window.devicePixelRatio || 1;
|
| const w = canvas.clientWidth;
|
| const h = canvas.clientHeight;
|
| canvas.width = w * dpr;
|
| canvas.height = h * dpr;
|
| ctx.scale(dpr, dpr);
|
|
|
| ctx.clearRect(0, 0, w, h);
|
|
|
| const min = Math.min(...data) * 0.9;
|
| const max = Math.max(...data) * 1.1 || 1;
|
| const range = max - min || 1;
|
|
|
| ctx.beginPath();
|
| ctx.strokeStyle = color;
|
| ctx.lineWidth = 1.5;
|
|
|
| for (let i = 0; i < data.length; i++) {
|
| const x = (i / (data.length - 1)) * w;
|
| const y = h - ((data[i] - min) / range) * h;
|
| if (i === 0) ctx.moveTo(x, y);
|
| else ctx.lineTo(x, y);
|
| }
|
| ctx.stroke();
|
|
|
|
|
| ctx.lineTo(w, h);
|
| ctx.lineTo(0, h);
|
| ctx.closePath();
|
| ctx.fillStyle = color
|
| .replace(")", ", 0.1)")
|
| .replace("var(", "rgba(88, 166, 255");
|
|
|
| ctx.globalAlpha = 0.1;
|
| ctx.fillStyle = color === "var(--yellow)" ? "#d29922" : "#58a6ff";
|
| ctx.fill();
|
| ctx.globalAlpha = 1;
|
| }
|
|
|
| function drawCorrectChart() {
|
| const canvas = document.getElementById("correctChart");
|
| if (!canvas || state.correctHistory.length === 0) return;
|
|
|
| const ctx = canvas.getContext("2d");
|
| const dpr = window.devicePixelRatio || 1;
|
| const w = canvas.clientWidth;
|
| const h = canvas.clientHeight;
|
| canvas.width = w * dpr;
|
| canvas.height = h * dpr;
|
| ctx.scale(dpr, dpr);
|
|
|
| ctx.clearRect(0, 0, w, h);
|
|
|
| const last = state.correctHistory.slice(-40);
|
| const barW = w / 40;
|
|
|
| for (let i = 0; i < last.length; i++) {
|
| ctx.fillStyle = last[i] ? "#3fb950" : "#f85149";
|
| ctx.fillRect(i * barW + 1, 2, barW - 2, h - 4);
|
| }
|
| }
|
|
|
|
|
| window.addEventListener("load", () => {
|
| connectSSE();
|
| addMsg(
|
| "system",
|
| "Conectado al servidor. Presiona βΆ Iniciar para comenzar la clase.",
|
| );
|
| });
|
| </script>
|
| </body>
|
| </html>
|
|
|