@bis-code/study-dash 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -919,9 +919,6 @@ function registerExerciseTools(server2, svc, sessions2, notify2) {
919
919
 
920
920
  // src/dashboard/server.ts
921
921
  import http from "node:http";
922
- import { readFileSync as readFileSync2 } from "node:fs";
923
- import { fileURLToPath } from "node:url";
924
- import { dirname, join as join2 } from "node:path";
925
922
 
926
923
  // src/dashboard/api.ts
927
924
  function writeJSON(res, data, status = 200) {
@@ -1066,25 +1063,1046 @@ function handleSearch(qaSvc2) {
1066
1063
  };
1067
1064
  }
1068
1065
 
1069
- // src/dashboard/server.ts
1070
- var __filename = fileURLToPath(import.meta.url);
1071
- var __dirname = dirname(__filename);
1072
- var staticDir = join2(__dirname, "static");
1073
- function loadStatic(name) {
1066
+ // src/dashboard/static/index.html
1067
+ var static_default = `<!DOCTYPE html>
1068
+ <html lang="en">
1069
+ <head>
1070
+ <meta charset="UTF-8">
1071
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1072
+ <title>StudyDash</title>
1073
+ <link rel="stylesheet" href="styles.css">
1074
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
1075
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
1076
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
1077
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
1078
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js"></script>
1079
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/sql.min.js"></script>
1080
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js"></script>
1081
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js"></script>
1082
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/typescript.min.js"></script>
1083
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
1084
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js"></script>
1085
+ </head>
1086
+ <body>
1087
+
1088
+ <!-- Desktop sidebar (hidden on mobile) -->
1089
+ <div id="desktop-sidebar">
1090
+ <div class="sidebar-inner">
1091
+ <h1 style="font-size:18px;font-weight:700;color:var(--accent);margin-bottom:12px;">StudyDash</h1>
1092
+ <div id="sidebar-subjects" class="subject-switcher"></div>
1093
+ <div class="progress-bar" style="margin-bottom:14px;">
1094
+ <div id="sidebar-progress-fill" class="progress-fill" style="width:0%"></div>
1095
+ <span id="sidebar-progress-text" class="progress-text">0 / 0 topics</span>
1096
+ </div>
1097
+ <div id="sidebar-phases"></div>
1098
+ </div>
1099
+ <div class="sidebar-footer">
1100
+ <span class="sse-dot disconnected" id="sse-dot"></span>
1101
+ <kbd>Ctrl+K</kbd> Search
1102
+ </div>
1103
+ </div>
1104
+
1105
+ <div class="page-container">
1106
+
1107
+ <!-- ==================== PAGE: HOME ==================== -->
1108
+ <div id="page-home" class="page active">
1109
+ <div class="page-header">
1110
+ <h1>StudyDash</h1>
1111
+ </div>
1112
+ <div id="home-subjects" class="subject-switcher"></div>
1113
+ <div class="progress-bar">
1114
+ <div id="home-progress-fill" class="progress-fill" style="width:0%"></div>
1115
+ <span id="home-progress-text" class="progress-text">0 / 0 topics</span>
1116
+ </div>
1117
+ <div id="home-stats" class="stats-grid"></div>
1118
+ <div class="section-divider">Recently Active</div>
1119
+ <div id="home-recent"></div>
1120
+ </div>
1121
+
1122
+ <!-- ==================== PAGE: TOPICS ==================== -->
1123
+ <div id="page-topics" class="page">
1124
+ <div class="page-header">
1125
+ <h1>Topics</h1>
1126
+ </div>
1127
+ <div id="topics-subjects" class="subject-switcher"></div>
1128
+ <div id="topics-phases"></div>
1129
+ </div>
1130
+
1131
+ <!-- ==================== PAGE: TOPIC DETAIL ==================== -->
1132
+ <div id="page-topic" class="page">
1133
+ <button class="back-btn" onclick="showPage('topics')">&larr; Back to Topics</button>
1134
+ <div class="topic-title-row">
1135
+ <h2 id="topic-name"></h2>
1136
+ <span id="topic-status" class="badge"></span>
1137
+ </div>
1138
+ <p id="topic-desc" class="topic-desc"></p>
1139
+ <div class="tabs">
1140
+ <button class="tab-btn active" data-tab="qa" onclick="switchTab('qa')">Q&amp;A</button>
1141
+ <button class="tab-btn" data-tab="viz" onclick="switchTab('viz')">Visualize</button>
1142
+ <button class="tab-btn" data-tab="exercises" onclick="switchTab('exercises')">Exercises</button>
1143
+ <button class="tab-btn" data-tab="resources" onclick="switchTab('resources')">Resources</button>
1144
+ </div>
1145
+ <div id="tab-qa" class="tab-panel active"></div>
1146
+ <div id="tab-viz" class="tab-panel"></div>
1147
+ <div id="tab-exercises" class="tab-panel"></div>
1148
+ <div id="tab-resources" class="tab-panel"></div>
1149
+ </div>
1150
+
1151
+ <!-- ==================== PAGE: SEARCH ==================== -->
1152
+ <div id="page-search" class="page">
1153
+ <div class="page-header">
1154
+ <h1>Search</h1>
1155
+ </div>
1156
+ <div class="search-bar">
1157
+ <input type="text" id="search-input" placeholder="Search questions, answers, notes..." autocomplete="off">
1158
+ </div>
1159
+ <div id="search-results"></div>
1160
+ </div>
1161
+
1162
+ </div>
1163
+
1164
+ <!-- Mobile bottom nav -->
1165
+ <nav class="mobile-nav">
1166
+ <button class="nav-btn active" data-page="home" onclick="showPage('home')">
1167
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
1168
+ Home
1169
+ </button>
1170
+ <button class="nav-btn" data-page="topics" onclick="showPage('topics')">
1171
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
1172
+ Topics
1173
+ </button>
1174
+ <button class="nav-btn" data-page="search" onclick="showPage('search')">
1175
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
1176
+ Search
1177
+ </button>
1178
+ </nav>
1179
+
1180
+ <!-- Search modal (desktop Ctrl+K) -->
1181
+ <div id="search-modal" class="modal hidden">
1182
+ <div class="modal-backdrop" onclick="closeSearchModal()"></div>
1183
+ <div class="modal-content">
1184
+ <input id="modal-search-input" type="text" placeholder="Search questions, answers, notes..." autocomplete="off">
1185
+ <div id="modal-search-results" class="modal-results"></div>
1186
+ </div>
1187
+ </div>
1188
+
1189
+ <script src="app.js"></script>
1190
+ </body>
1191
+ </html>
1192
+ `;
1193
+
1194
+ // src/dashboard/static/app.js
1195
+ var app_default = `// StudyDash \u2014 Dashboard Application
1196
+ // Connects to the learn-cc API and renders a responsive learning dashboard.
1197
+
1198
+ // --- Markdown config ---
1199
+ marked.setOptions({
1200
+ highlight: function(code, lang) {
1201
+ if (lang && hljs.getLanguage(lang)) {
1202
+ return hljs.highlight(code, { language: lang }).value;
1203
+ }
1204
+ return hljs.highlightAuto(code).value;
1205
+ },
1206
+ breaks: true,
1207
+ gfm: true,
1208
+ });
1209
+
1210
+ // --- State ---
1211
+ const state = {
1212
+ subjects: [],
1213
+ activeSubject: null,
1214
+ phases: [],
1215
+ activeTopic: null,
1216
+ activeTab: 'qa',
1217
+ topicData: null,
1218
+ topicViz: [],
1219
+ topicExercises: [],
1220
+ searchTimeout: null,
1221
+ vizIndex: 0,
1222
+ vizStep: 0,
1223
+ };
1224
+
1225
+ // --- API Helper ---
1226
+ async function api(path) {
1227
+ const res = await fetch(path);
1228
+ if (!res.ok) throw new Error(\`API error: \${res.status}\`);
1229
+ return res.json();
1230
+ }
1231
+
1232
+ // --- Sanitize viz HTML ---
1233
+ function sanitizeVizHtml(html) {
1234
+ const allowed = ['div', 'span', 'small', 'br', 'code', 'strong', 'em'];
1235
+ const tmp = document.createElement('div');
1236
+ tmp.innerHTML = html;
1237
+ // Remove all script tags
1238
+ tmp.querySelectorAll('script').forEach(el => el.remove());
1239
+ // Walk all elements, remove disallowed tags, strip non-class/style attributes
1240
+ const walk = (node) => {
1241
+ const children = [...node.children];
1242
+ for (const child of children) {
1243
+ if (!allowed.includes(child.tagName.toLowerCase())) {
1244
+ child.replaceWith(...child.childNodes);
1245
+ } else {
1246
+ [...child.attributes].forEach(attr => {
1247
+ if (attr.name !== 'class' && attr.name !== 'style') child.removeAttribute(attr.name);
1248
+ });
1249
+ walk(child);
1250
+ }
1251
+ }
1252
+ };
1253
+ walk(tmp);
1254
+ return tmp.innerHTML;
1255
+ }
1256
+
1257
+ // --- Escape HTML for text content in templates ---
1258
+ function escapeHtml(str) {
1259
+ if (!str) return '';
1260
+ const div = document.createElement('div');
1261
+ div.textContent = str;
1262
+ return div.innerHTML;
1263
+ }
1264
+
1265
+ // --- Format time ---
1266
+ function formatTime(iso) {
1267
+ if (!iso) return '';
1268
+ const d = new Date(iso);
1269
+ return d.toLocaleDateString('en-US', {
1270
+ month: 'short', day: 'numeric', year: 'numeric',
1271
+ hour: '2-digit', minute: '2-digit',
1272
+ });
1273
+ }
1274
+
1275
+ function truncate(text, max) {
1276
+ if (!text) return '';
1277
+ if (text.length <= max) return escapeHtml(text);
1278
+ return escapeHtml(text.substring(0, max)) + '...';
1279
+ }
1280
+
1281
+ // --- On load ---
1282
+ document.addEventListener('DOMContentLoaded', async () => {
1283
+ try {
1284
+ state.subjects = await api('/api/subjects');
1285
+ } catch {
1286
+ state.subjects = [];
1287
+ }
1288
+
1289
+ if (state.subjects.length > 0) {
1290
+ state.activeSubject = state.subjects[0];
1291
+ await loadSubject();
1292
+ }
1293
+
1294
+ renderAllSubjectSwitchers();
1295
+ renderHome();
1296
+ connectSSE();
1297
+ });
1298
+
1299
+ // --- SSE ---
1300
+ function connectSSE() {
1301
+ const dot = document.getElementById('sse-dot');
1302
+ let evtSource;
1303
+
1304
+ function connect() {
1305
+ evtSource = new EventSource('/api/events');
1306
+
1307
+ evtSource.onopen = () => {
1308
+ if (dot) { dot.classList.remove('disconnected'); dot.classList.add('connected'); }
1309
+ };
1310
+
1311
+ evtSource.onmessage = async (event) => {
1312
+ try {
1313
+ const data = JSON.parse(event.data);
1314
+ if (data.type === 'update') {
1315
+ await refresh();
1316
+ }
1317
+ } catch { /* ignore parse errors */ }
1318
+ };
1319
+
1320
+ evtSource.onerror = () => {
1321
+ if (dot) { dot.classList.remove('connected'); dot.classList.add('disconnected'); }
1322
+ evtSource.close();
1323
+ setTimeout(connect, 3000);
1324
+ };
1325
+ }
1326
+
1327
+ connect();
1328
+ }
1329
+
1330
+ async function refresh() {
1331
+ // Re-fetch subjects
1332
+ try {
1333
+ state.subjects = await api('/api/subjects');
1334
+ } catch { /* keep existing */ }
1335
+
1336
+ if (state.activeSubject) {
1337
+ // Refresh the active subject from the new list
1338
+ const updated = state.subjects.find(s => s.id === state.activeSubject.id);
1339
+ if (updated) state.activeSubject = updated;
1340
+ await loadSubject();
1341
+ }
1342
+
1343
+ renderAllSubjectSwitchers();
1344
+
1345
+ // Re-render current view
1346
+ const activePage = document.querySelector('.page.active');
1347
+ if (activePage) {
1348
+ const pageId = activePage.id.replace('page-', '');
1349
+ if (pageId === 'home') renderHome();
1350
+ else if (pageId === 'topics') renderTopicsPage();
1351
+ else if (pageId === 'topic' && state.activeTopic) await selectTopic(state.activeTopic);
1352
+ }
1353
+ }
1354
+
1355
+ // --- Subject management ---
1356
+ async function loadSubject() {
1357
+ if (!state.activeSubject) return;
1358
+ try {
1359
+ state.phases = await api(\`/api/subjects/\${state.activeSubject.id}/phases\`);
1360
+ } catch {
1361
+ state.phases = [];
1362
+ }
1363
+ renderSidebar();
1364
+ }
1365
+
1366
+ async function switchSubject(id) {
1367
+ const subject = state.subjects.find(s => s.id === id);
1368
+ if (!subject) return;
1369
+ state.activeSubject = subject;
1370
+ state.activeTopic = null;
1371
+ state.topicData = null;
1372
+ await loadSubject();
1373
+ renderAllSubjectSwitchers();
1374
+ renderHome();
1375
+ renderTopicsPage();
1376
+ // If on topic detail page, go back to topics
1377
+ const activePage = document.querySelector('.page.active');
1378
+ if (activePage && activePage.id === 'page-topic') {
1379
+ showPage('topics');
1380
+ }
1381
+ }
1382
+
1383
+ function renderAllSubjectSwitchers() {
1384
+ const containers = ['home-subjects', 'topics-subjects', 'sidebar-subjects'];
1385
+ containers.forEach(id => {
1386
+ const el = document.getElementById(id);
1387
+ if (!el) return;
1388
+ if (state.subjects.length === 0) {
1389
+ el.innerHTML = '';
1390
+ return;
1391
+ }
1392
+ el.innerHTML = state.subjects.map(s =>
1393
+ \`<button class="subject-btn \${state.activeSubject && state.activeSubject.id === s.id ? 'active' : ''}"
1394
+ onclick="switchSubject(\${s.id})">\${escapeHtml(s.name)}</button>\`
1395
+ ).join('');
1396
+ });
1397
+ }
1398
+
1399
+ // --- Progress ---
1400
+ function renderProgress() {
1401
+ if (!state.activeSubject) return;
1402
+ const p = state.activeSubject.progress || {};
1403
+ const total = p.total_topics || 0;
1404
+ const done = p.done || 0;
1405
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
1406
+ const text = \`\${done} / \${total} topics\`;
1407
+
1408
+ // Home progress
1409
+ const hFill = document.getElementById('home-progress-fill');
1410
+ const hText = document.getElementById('home-progress-text');
1411
+ if (hFill) hFill.style.width = pct + '%';
1412
+ if (hText) hText.textContent = text;
1413
+
1414
+ // Sidebar progress
1415
+ const sFill = document.getElementById('sidebar-progress-fill');
1416
+ const sText = document.getElementById('sidebar-progress-text');
1417
+ if (sFill) sFill.style.width = pct + '%';
1418
+ if (sText) sText.textContent = text;
1419
+ }
1420
+
1421
+ // --- Sidebar ---
1422
+ function renderSidebar() {
1423
+ renderProgress();
1424
+ const container = document.getElementById('sidebar-phases');
1425
+ if (!container) return;
1426
+
1427
+ if (state.phases.length === 0) {
1428
+ container.innerHTML = '<div class="empty-state"><p>No phases yet</p></div>';
1429
+ return;
1430
+ }
1431
+
1432
+ container.innerHTML = state.phases.map(phase => {
1433
+ const topics = phase.topics || [];
1434
+ const doneCount = topics.filter(t => t.status === 'done').length;
1435
+ return \`
1436
+ <div class="phase-group">
1437
+ <div class="phase-header" onclick="togglePhase(this)">
1438
+ \${escapeHtml(phase.name)}
1439
+ <span style="font-size:10px;color:var(--text-muted)">\${doneCount}/\${topics.length}</span>
1440
+ <span class="chevron">&#9660;</span>
1441
+ </div>
1442
+ <div class="phase-topics">
1443
+ \${topics.map(t => \`
1444
+ <div class="topic-item \${state.activeTopic === t.id ? 'active' : ''}"
1445
+ onclick="selectTopic(\${t.id})"
1446
+ data-topic-id="\${t.id}">
1447
+ <span class="status-dot \${escapeHtml(t.status)}"></span>
1448
+ <span>\${escapeHtml(t.name)}</span>
1449
+ </div>
1450
+ \`).join('')}
1451
+ </div>
1452
+ </div>\`;
1453
+ }).join('');
1454
+ }
1455
+
1456
+ function togglePhase(el) {
1457
+ el.classList.toggle('collapsed');
1458
+ el.nextElementSibling.classList.toggle('collapsed');
1459
+ }
1460
+
1461
+ // --- Home page ---
1462
+ function renderHome() {
1463
+ renderProgress();
1464
+
1465
+ const statsEl = document.getElementById('home-stats');
1466
+ const recentEl = document.getElementById('home-recent');
1467
+
1468
+ if (state.subjects.length === 0) {
1469
+ if (statsEl) statsEl.innerHTML = '';
1470
+ if (recentEl) recentEl.innerHTML = \`
1471
+ <div class="empty-state">
1472
+ <p>Welcome to StudyDash!</p>
1473
+ <p>Start by creating a subject with <code>/learn</code></p>
1474
+ </div>\`;
1475
+ return;
1476
+ }
1477
+
1478
+ const p = state.activeSubject ? (state.activeSubject.progress || {}) : {};
1479
+
1480
+ if (statsEl) {
1481
+ statsEl.innerHTML = \`
1482
+ <div class="stat-card">
1483
+ <div class="stat-value">\${p.done || 0}</div>
1484
+ <div class="stat-label">Topics Done</div>
1485
+ </div>
1486
+ <div class="stat-card">
1487
+ <div class="stat-value green">\${p.total_entries || 0}</div>
1488
+ <div class="stat-label">Q&amp;A Entries</div>
1489
+ </div>
1490
+ <div class="stat-card">
1491
+ <div class="stat-value yellow">\${p.total_exercises || 0}</div>
1492
+ <div class="stat-label">Exercises</div>
1493
+ </div>
1494
+ <div class="stat-card">
1495
+ <div class="stat-value purple">\${p.total_viz || 0}</div>
1496
+ <div class="stat-label">Visualizations</div>
1497
+ </div>\`;
1498
+ }
1499
+
1500
+ // Show recently active topics (in_progress first, then by updated_at)
1501
+ if (recentEl) {
1502
+ const allTopics = state.phases.flatMap(ph => (ph.topics || []).map(t => ({ ...t, phaseName: ph.name })));
1503
+ const active = allTopics
1504
+ .filter(t => t.status !== 'todo')
1505
+ .sort((a, b) => {
1506
+ if (a.status === 'in_progress' && b.status !== 'in_progress') return -1;
1507
+ if (b.status === 'in_progress' && a.status !== 'in_progress') return 1;
1508
+ return new Date(b.updated_at || 0) - new Date(a.updated_at || 0);
1509
+ })
1510
+ .slice(0, 5);
1511
+
1512
+ if (active.length === 0) {
1513
+ recentEl.innerHTML = \`<div class="empty-state"><p>No active topics yet. Import a curriculum with <code>/learn import</code></p></div>\`;
1514
+ } else {
1515
+ recentEl.innerHTML = active.map(t => \`
1516
+ <div class="exercise-card" style="cursor:pointer" onclick="selectTopic(\${t.id})">
1517
+ <div class="exercise-header">
1518
+ <span class="exercise-title">\${escapeHtml(t.name)}</span>
1519
+ <span class="badge \${escapeHtml(t.status)}">\${escapeHtml(t.status.replace('_', ' '))}</span>
1520
+ </div>
1521
+ <div class="exercise-desc">\${escapeHtml(t.phaseName)}</div>
1522
+ </div>
1523
+ \`).join('');
1524
+ }
1525
+ }
1526
+ }
1527
+
1528
+ // --- Topics page ---
1529
+ function renderTopicsPage() {
1530
+ const container = document.getElementById('topics-phases');
1531
+ if (!container) return;
1532
+
1533
+ if (state.phases.length === 0) {
1534
+ container.innerHTML = \`<div class="empty-state"><p>No topics yet.</p><p>Import a curriculum with <code>/learn import</code></p></div>\`;
1535
+ return;
1536
+ }
1537
+
1538
+ container.innerHTML = state.phases.map(phase => {
1539
+ const topics = phase.topics || [];
1540
+ return \`
1541
+ <div class="phase-group">
1542
+ <div class="phase-header" onclick="togglePhase(this)">
1543
+ \${escapeHtml(phase.name)}
1544
+ <span class="chevron">&#9660;</span>
1545
+ </div>
1546
+ <div class="phase-topics">
1547
+ \${topics.map(t => \`
1548
+ <div class="topic-item \${state.activeTopic === t.id ? 'active' : ''}"
1549
+ onclick="selectTopic(\${t.id})">
1550
+ <span class="status-dot \${escapeHtml(t.status)}"></span>
1551
+ \${escapeHtml(t.name)}
1552
+ <span class="topic-count"></span>
1553
+ </div>
1554
+ \`).join('')}
1555
+ </div>
1556
+ </div>\`;
1557
+ }).join('');
1558
+ }
1559
+
1560
+ // --- Topic selection ---
1561
+ async function selectTopic(id) {
1562
+ state.activeTopic = id;
1563
+ state.activeTab = 'qa';
1564
+
1565
+ // Fetch topic detail, viz, and exercises in parallel
1566
+ try {
1567
+ const [topicData, viz, exercises] = await Promise.all([
1568
+ api(\`/api/topics/\${id}\`),
1569
+ api(\`/api/topics/\${id}/viz\`).catch(() => []),
1570
+ api(\`/api/topics/\${id}/exercises\`).catch(() => []),
1571
+ ]);
1572
+
1573
+ state.topicData = topicData;
1574
+ state.topicViz = viz || [];
1575
+ state.topicExercises = exercises || [];
1576
+ } catch {
1577
+ state.topicData = null;
1578
+ state.topicViz = [];
1579
+ state.topicExercises = [];
1580
+ }
1581
+
1582
+ // Update sidebar active state
1583
+ document.querySelectorAll('.topic-item').forEach(el => {
1584
+ el.classList.toggle('active', parseInt(el.dataset?.topicId) === id);
1585
+ });
1586
+
1587
+ showPage('topic');
1588
+ renderTopicDetail();
1589
+ switchTab('qa');
1590
+ }
1591
+
1592
+ function renderTopicDetail() {
1593
+ const data = state.topicData;
1594
+ if (!data) return;
1595
+
1596
+ document.getElementById('topic-name').textContent = data.name || '';
1597
+ const statusEl = document.getElementById('topic-status');
1598
+ statusEl.textContent = (data.status || '').replace('_', ' ');
1599
+ statusEl.className = \`badge \${data.status || ''}\`;
1600
+ document.getElementById('topic-desc').textContent = data.description || '';
1601
+ }
1602
+
1603
+ // --- Tab switching ---
1604
+ function switchTab(tab) {
1605
+ state.activeTab = tab;
1606
+
1607
+ document.querySelectorAll('#page-topic .tab-btn').forEach(btn => {
1608
+ btn.classList.toggle('active', btn.dataset.tab === tab);
1609
+ });
1610
+
1611
+ document.querySelectorAll('#page-topic .tab-panel').forEach(panel => {
1612
+ panel.classList.toggle('active', panel.id === \`tab-\${tab}\`);
1613
+ });
1614
+
1615
+ // Render tab content
1616
+ if (tab === 'qa') renderQATab();
1617
+ else if (tab === 'viz') renderVizTab();
1618
+ else if (tab === 'exercises') renderExercisesTab();
1619
+ else if (tab === 'resources') renderResourcesTab();
1620
+ }
1621
+
1622
+ // --- Q&A Tab ---
1623
+ function renderQATab() {
1624
+ const container = document.getElementById('tab-qa');
1625
+ if (!container) return;
1626
+
1627
+ const entries = state.topicData?.entries || [];
1628
+
1629
+ if (entries.length === 0) {
1630
+ container.innerHTML = \`<div class="empty-state"><p>No Q&amp;A entries yet</p><p>Ask questions in Claude to see them here</p></div>\`;
1631
+ return;
1632
+ }
1633
+
1634
+ // Group entries into Q&A cards by question_id
1635
+ const questionMap = new Map();
1636
+ const groups = [];
1637
+
1638
+ entries.forEach(e => {
1639
+ if (e.kind === 'question') {
1640
+ const group = { question: e, answers: [] };
1641
+ questionMap.set(e.id, group);
1642
+ groups.push(group);
1643
+ } else if (e.question_id && questionMap.has(e.question_id)) {
1644
+ questionMap.get(e.question_id).answers.push(e);
1645
+ } else {
1646
+ groups.push({ standalone: e });
1647
+ }
1648
+ });
1649
+
1650
+ // marked.parse is used intentionally for markdown rendering (same as go-learn)
1651
+ container.innerHTML = groups.map(g => {
1652
+ if (g.standalone) {
1653
+ const e = g.standalone;
1654
+ return \`
1655
+ <div class="entry-card">
1656
+ <div class="entry-header">
1657
+ <span class="entry-kind \${escapeHtml(e.kind)}">\${escapeHtml(e.kind)}</span>
1658
+ <span>\${formatTime(e.created_at)}</span>
1659
+ </div>
1660
+ <div class="entry-body">\${marked.parse(e.content || '')}</div>
1661
+ </div>\`;
1662
+ }
1663
+
1664
+ const q = g.question;
1665
+ let html = \`<div class="qa-card">\`;
1666
+ html += \`
1667
+ <div class="qa-question">
1668
+ <div class="entry-header">
1669
+ <span class="entry-kind question">question</span>
1670
+ <span>\${formatTime(q.created_at)}</span>
1671
+ </div>
1672
+ <div class="entry-body">\${marked.parse(q.content || '')}</div>
1673
+ </div>\`;
1674
+
1675
+ g.answers.forEach(a => {
1676
+ html += \`
1677
+ <div class="qa-answer">
1678
+ <div class="entry-header">
1679
+ <span class="entry-kind \${escapeHtml(a.kind)}">\${escapeHtml(a.kind)}</span>
1680
+ <span>\${formatTime(a.created_at)}</span>
1681
+ </div>
1682
+ <div class="entry-body">\${marked.parse(a.content || '')}</div>
1683
+ </div>\`;
1684
+ });
1685
+
1686
+ html += \`</div>\`;
1687
+ return html;
1688
+ }).join('');
1689
+ }
1690
+
1691
+ // --- Visualize Tab ---
1692
+ function renderVizTab() {
1693
+ const container = document.getElementById('tab-viz');
1694
+ if (!container) return;
1695
+
1696
+ const vizList = state.topicViz;
1697
+
1698
+ if (!vizList || vizList.length === 0) {
1699
+ container.innerHTML = \`<div class="empty-state"><p>No visualizations yet</p><p>Visualizations will appear here as you learn</p></div>\`;
1700
+ return;
1701
+ }
1702
+
1703
+ // Reset viz state
1704
+ state.vizIndex = 0;
1705
+ state.vizStep = 0;
1706
+
1707
+ renderVizSelector(container);
1708
+ }
1709
+
1710
+ function renderVizSelector(container) {
1711
+ if (!container) container = document.getElementById('tab-viz');
1712
+ if (!container) return;
1713
+
1714
+ const vizList = state.topicViz;
1715
+ if (!vizList || vizList.length === 0) return;
1716
+
1717
+ let html = \`<div class="viz-selector">\`;
1718
+ vizList.forEach((v, i) => {
1719
+ html += \`<button class="viz-select-btn \${i === state.vizIndex ? 'active' : ''}" onclick="selectViz(\${i})">\${escapeHtml(v.title)}</button>\`;
1720
+ });
1721
+ html += \`</div>\`;
1722
+ html += \`<div id="viz-stage-container"></div>\`;
1723
+
1724
+ container.innerHTML = html;
1725
+ renderVizStage();
1726
+ }
1727
+
1728
+ function selectViz(index) {
1729
+ state.vizIndex = index;
1730
+ state.vizStep = 0;
1731
+
1732
+ // Update selector buttons
1733
+ document.querySelectorAll('.viz-select-btn').forEach((btn, i) => {
1734
+ btn.classList.toggle('active', i === index);
1735
+ });
1736
+
1737
+ renderVizStage();
1738
+ }
1739
+
1740
+ function renderVizStage() {
1741
+ const stageContainer = document.getElementById('viz-stage-container');
1742
+ if (!stageContainer) return;
1743
+
1744
+ const viz = state.topicViz[state.vizIndex];
1745
+ if (!viz) return;
1746
+
1747
+ let steps;
1074
1748
  try {
1075
- return readFileSync2(join2(staticDir, name), "utf-8");
1749
+ steps = typeof viz.steps_json === 'string' ? JSON.parse(viz.steps_json) : viz.steps_json;
1076
1750
  } catch {
1077
- return "";
1751
+ stageContainer.innerHTML = \`<div class="empty-state"><p>Invalid visualization data</p></div>\`;
1752
+ return;
1753
+ }
1754
+
1755
+ if (!steps || steps.length === 0) {
1756
+ stageContainer.innerHTML = \`<div class="empty-state"><p>No steps in this visualization</p></div>\`;
1757
+ return;
1758
+ }
1759
+
1760
+ const step = steps[state.vizStep] || steps[0];
1761
+ const totalSteps = steps.length;
1762
+
1763
+ // sanitizeVizHtml strips dangerous content, allowing only safe tags with class/style
1764
+ stageContainer.innerHTML = \`
1765
+ <div class="viz-stage">
1766
+ <div class="viz-canvas">\${sanitizeVizHtml(step.html || step.canvas || '')}</div>
1767
+ \${step.description || step.desc ? \`<div class="viz-description">\${sanitizeVizHtml(step.description || step.desc || '')}</div>\` : ''}
1768
+ <div class="viz-controls">
1769
+ <button onclick="vizPrev()" \${state.vizStep === 0 ? 'disabled' : ''}>Prev</button>
1770
+ <span class="viz-step-label">Step \${state.vizStep + 1} / \${totalSteps}</span>
1771
+ <button onclick="vizNext()" \${state.vizStep >= totalSteps - 1 ? 'disabled' : ''}>Next</button>
1772
+ </div>
1773
+ </div>\`;
1774
+ }
1775
+
1776
+ function vizPrev() {
1777
+ if (state.vizStep > 0) {
1778
+ state.vizStep--;
1779
+ renderVizStage();
1078
1780
  }
1079
1781
  }
1080
- var indexHtml = loadStatic("index.html");
1081
- var appJs = loadStatic("app.js");
1082
- var stylesCss = loadStatic("styles.css");
1782
+
1783
+ function vizNext() {
1784
+ const viz = state.topicViz[state.vizIndex];
1785
+ if (!viz) return;
1786
+ let steps;
1787
+ try {
1788
+ steps = typeof viz.steps_json === 'string' ? JSON.parse(viz.steps_json) : viz.steps_json;
1789
+ } catch { return; }
1790
+ if (state.vizStep < (steps?.length || 1) - 1) {
1791
+ state.vizStep++;
1792
+ renderVizStage();
1793
+ }
1794
+ }
1795
+
1796
+ // --- Exercises Tab ---
1797
+ function renderExercisesTab() {
1798
+ const container = document.getElementById('tab-exercises');
1799
+ if (!container) return;
1800
+
1801
+ const exercises = state.topicExercises;
1802
+
1803
+ if (!exercises || exercises.length === 0) {
1804
+ container.innerHTML = \`<div class="empty-state"><p>No exercises yet</p><p>Exercises are generated when you complete topics</p></div>\`;
1805
+ return;
1806
+ }
1807
+
1808
+ container.innerHTML = exercises.map((ex, i) => {
1809
+ const results = ex.results || [];
1810
+ const passed = results.filter(r => r.passed).length;
1811
+ const total = results.length;
1812
+ const hasPassed = total > 0 && passed === total;
1813
+
1814
+ let detailHtml = '';
1815
+
1816
+ // Quiz type
1817
+ if (ex.type === 'quiz' && ex.quiz_json) {
1818
+ let quiz;
1819
+ try {
1820
+ quiz = typeof ex.quiz_json === 'string' ? JSON.parse(ex.quiz_json) : ex.quiz_json;
1821
+ } catch { quiz = null; }
1822
+
1823
+ if (quiz && Array.isArray(quiz)) {
1824
+ detailHtml += \`<h4>Questions</h4>\`;
1825
+ detailHtml += quiz.map((q, qi) => \`
1826
+ <div class="quiz-question" data-exercise="\${i}" data-question="\${qi}">
1827
+ <p>\${marked.parse(q.question || q.text || '')}</p>
1828
+ \${(q.options || q.choices || []).map((opt, oi) => \`
1829
+ <div class="quiz-option" data-exercise="\${i}" data-question="\${qi}" data-option="\${oi}" onclick="selectQuizOption(this)">
1830
+ \${escapeHtml(opt)}
1831
+ </div>
1832
+ \`).join('')}
1833
+ </div>
1834
+ \`).join('');
1835
+ detailHtml += \`
1836
+ <div class="exercise-actions">
1837
+ <button class="exercise-action-btn btn-primary" onclick="submitQuiz(\${ex.id}, \${i})">Submit Answers</button>
1838
+ </div>\`;
1839
+ }
1840
+ }
1841
+
1842
+ // Coding type \u2014 test cases
1843
+ if (ex.type === 'coding' || ex.type === 'project' || ex.type === 'assignment') {
1844
+ if (results.length > 0) {
1845
+ detailHtml += \`<h4>Test Results</h4>\`;
1846
+ detailHtml += results.map(r => \`
1847
+ <div class="test-case">
1848
+ <div class="test-case-header">
1849
+ <span class="test-status \${r.passed ? 'pass' : 'fail'}"></span>
1850
+ \${escapeHtml(r.test_name)}
1851
+ </div>
1852
+ \${r.output ? \`<div class="test-case-body">\${truncate(r.output, 300)}</div>\` : ''}
1853
+ </div>
1854
+ \`).join('');
1855
+
1856
+ detailHtml += \`
1857
+ <div class="exercise-progress">
1858
+ <span>\${passed}/\${total} tests</span>
1859
+ <div class="exercise-progress-bar">
1860
+ <div class="exercise-progress-fill \${hasPassed ? 'green' : 'yellow'}" style="width:\${total > 0 ? Math.round(passed / total * 100) : 0}%"></div>
1861
+ </div>
1862
+ </div>\`;
1863
+ }
1864
+
1865
+ detailHtml += \`
1866
+ <div class="exercise-actions">
1867
+ <button class="exercise-action-btn btn-primary" onclick="runExercise(\${ex.id}, \${i})">Run Tests</button>
1868
+ </div>\`;
1869
+ }
1870
+
1871
+ return \`
1872
+ <div class="exercise-card expandable" id="exercise-\${i}">
1873
+ <div class="exercise-header" onclick="toggleExercise(\${i})">
1874
+ <span class="exercise-title">\${escapeHtml(ex.title)}</span>
1875
+ <span class="exercise-type \${escapeHtml(ex.type)}">\${escapeHtml(ex.type)}</span>
1876
+ <span class="exercise-expand-icon">&#9660;</span>
1877
+ </div>
1878
+ <div class="exercise-desc">\${escapeHtml(ex.description || '')}</div>
1879
+ <div class="exercise-meta">
1880
+ \${ex.difficulty ? \`<span>Difficulty: \${escapeHtml(ex.difficulty)}</span>\` : ''}
1881
+ \${ex.est_minutes ? \`<span>\${ex.est_minutes} min</span>\` : ''}
1882
+ \${ex.source ? \`<span>Source: \${escapeHtml(ex.source)}</span>\` : ''}
1883
+ \${ex.status ? \`<span>Status: \${escapeHtml(ex.status)}</span>\` : ''}
1884
+ </div>
1885
+ <div class="exercise-detail" id="exercise-detail-\${i}">
1886
+ \${detailHtml}
1887
+ </div>
1888
+ </div>\`;
1889
+ }).join('');
1890
+ }
1891
+
1892
+ function toggleExercise(index) {
1893
+ const detail = document.getElementById(\`exercise-detail-\${index}\`);
1894
+ if (detail) detail.classList.toggle('open');
1895
+ }
1896
+
1897
+ function selectQuizOption(el) {
1898
+ const questionEl = el.closest('.quiz-question');
1899
+ if (questionEl) {
1900
+ questionEl.querySelectorAll('.quiz-option').forEach(opt => opt.classList.remove('selected'));
1901
+ }
1902
+ el.classList.add('selected');
1903
+ }
1904
+
1905
+ async function submitQuiz(exerciseId, cardIndex) {
1906
+ const card = document.getElementById(\`exercise-\${cardIndex}\`);
1907
+ if (!card) return;
1908
+
1909
+ const answers = [];
1910
+ card.querySelectorAll('.quiz-question').forEach(q => {
1911
+ const selected = q.querySelector('.quiz-option.selected');
1912
+ if (selected) {
1913
+ answers.push(parseInt(selected.dataset.option));
1914
+ } else {
1915
+ answers.push(-1);
1916
+ }
1917
+ });
1918
+
1919
+ try {
1920
+ const result = await fetch(\`/api/exercises/\${exerciseId}/submit\`, {
1921
+ method: 'POST',
1922
+ headers: { 'Content-Type': 'application/json' },
1923
+ body: JSON.stringify({ answers }),
1924
+ });
1925
+ const data = await result.json();
1926
+
1927
+ if (data.results) {
1928
+ data.results.forEach((r, i) => {
1929
+ const questionEl = card.querySelectorAll('.quiz-question')[i];
1930
+ if (!questionEl) return;
1931
+ questionEl.querySelectorAll('.quiz-option').forEach((opt, oi) => {
1932
+ opt.classList.remove('selected');
1933
+ if (oi === r.correct_index) opt.classList.add('correct');
1934
+ else if (oi === answers[i] && !r.passed) opt.classList.add('incorrect');
1935
+ });
1936
+ });
1937
+ }
1938
+
1939
+ if (data.score !== undefined) {
1940
+ const actionsEl = card.querySelector('.exercise-actions');
1941
+ if (actionsEl) {
1942
+ const scoreDiv = document.createElement('div');
1943
+ scoreDiv.style.cssText = \`margin-top:8px;font-size:14px;font-weight:600;color:\${data.passed ? 'var(--green)' : 'var(--yellow)'}\`;
1944
+ scoreDiv.textContent = \`Score: \${data.score}/\${data.total}\${data.passed ? ' - Passed!' : ''}\`;
1945
+ actionsEl.appendChild(scoreDiv);
1946
+ }
1947
+ }
1948
+ } catch (err) {
1949
+ console.error('Submit quiz error:', err);
1950
+ }
1951
+ }
1952
+
1953
+ async function runExercise(exerciseId, cardIndex) {
1954
+ const btn = document.querySelector(\`#exercise-\${cardIndex} .btn-primary\`);
1955
+ if (btn) { btn.textContent = 'Running...'; btn.disabled = true; }
1956
+
1957
+ try {
1958
+ const res = await fetch(\`/api/exercises/\${exerciseId}/run\`, { method: 'POST' });
1959
+ const data = await res.json();
1960
+
1961
+ if (data.results && state.topicExercises[cardIndex]) {
1962
+ state.topicExercises[cardIndex].results = data.results;
1963
+ }
1964
+
1965
+ renderExercisesTab();
1966
+
1967
+ // Re-open the card
1968
+ const detail = document.getElementById(\`exercise-detail-\${cardIndex}\`);
1969
+ if (detail) detail.classList.add('open');
1970
+ } catch (err) {
1971
+ console.error('Run exercise error:', err);
1972
+ if (btn) { btn.textContent = 'Run Tests'; btn.disabled = false; }
1973
+ }
1974
+ }
1975
+
1976
+ // --- Resources Tab ---
1977
+ function renderResourcesTab() {
1978
+ const container = document.getElementById('tab-resources');
1979
+ if (!container) return;
1980
+
1981
+ const data = state.topicData;
1982
+ if (!data) {
1983
+ container.innerHTML = \`<div class="empty-state"><p>No resources available</p></div>\`;
1984
+ return;
1985
+ }
1986
+
1987
+ const entries = data.entries || [];
1988
+ const questions = entries.filter(e => e.kind === 'question').length;
1989
+ const answers = entries.filter(e => e.kind === 'answer').length;
1990
+ const notes = entries.filter(e => e.kind === 'note').length;
1991
+
1992
+ container.innerHTML = \`
1993
+ \${data.description ? \`<div class="exercise-card"><div style="padding:4px 0"><strong>Description</strong></div><p class="exercise-desc">\${escapeHtml(data.description)}</p></div>\` : ''}
1994
+ <div class="exercise-card">
1995
+ <div style="padding:4px 0"><strong>Content Summary</strong></div>
1996
+ <div class="exercise-meta" style="margin-top:8px">
1997
+ <span>Questions: \${questions}</span>
1998
+ <span>Answers: \${answers}</span>
1999
+ <span>Notes: \${notes}</span>
2000
+ <span>Visualizations: \${state.topicViz.length}</span>
2001
+ <span>Exercises: \${state.topicExercises.length}</span>
2002
+ </div>
2003
+ </div>\`;
2004
+ }
2005
+
2006
+ // --- Navigation ---
2007
+ function showPage(page) {
2008
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
2009
+
2010
+ const target = document.getElementById(\`page-\${page}\`);
2011
+ if (target) target.classList.add('active');
2012
+
2013
+ document.querySelectorAll('.nav-btn').forEach(btn => {
2014
+ btn.classList.toggle('active', btn.dataset.page === page);
2015
+ });
2016
+
2017
+ if (page === 'home') renderHome();
2018
+ else if (page === 'topics') renderTopicsPage();
2019
+ else if (page === 'search') document.getElementById('search-input')?.focus();
2020
+ }
2021
+
2022
+ // --- Search ---
2023
+ const searchInput = document.getElementById('search-input');
2024
+ if (searchInput) {
2025
+ searchInput.addEventListener('input', (e) => {
2026
+ clearTimeout(state.searchTimeout);
2027
+ state.searchTimeout = setTimeout(() => doSearch(e.target.value, 'search-results'), 200);
2028
+ });
2029
+ }
2030
+
2031
+ const modalSearchInput = document.getElementById('modal-search-input');
2032
+ if (modalSearchInput) {
2033
+ modalSearchInput.addEventListener('input', (e) => {
2034
+ clearTimeout(state.searchTimeout);
2035
+ state.searchTimeout = setTimeout(() => doSearch(e.target.value, 'modal-search-results'), 200);
2036
+ });
2037
+ }
2038
+
2039
+ async function doSearch(query, resultsContainerId) {
2040
+ const container = document.getElementById(resultsContainerId);
2041
+ if (!container) return;
2042
+
2043
+ if (!query || !query.trim()) {
2044
+ container.innerHTML = '';
2045
+ return;
2046
+ }
2047
+
2048
+ try {
2049
+ const results = await api(\`/api/search?q=\${encodeURIComponent(query)}\`);
2050
+
2051
+ if (!results || results.length === 0) {
2052
+ container.innerHTML = '<div class="search-no-results">No results found</div>';
2053
+ return;
2054
+ }
2055
+
2056
+ container.innerHTML = results.map(r => \`
2057
+ <div class="search-result-item" onclick="closeSearchModal(); selectTopic(\${r.topic_id})">
2058
+ <div class="search-result-meta">
2059
+ <span class="entry-kind \${escapeHtml(r.kind)}">\${escapeHtml(r.kind)}</span>
2060
+ </div>
2061
+ <div class="search-result-content">\${truncate(r.content, 150)}</div>
2062
+ </div>
2063
+ \`).join('');
2064
+ } catch {
2065
+ container.innerHTML = '<div class="search-no-results">Search failed</div>';
2066
+ }
2067
+ }
2068
+
2069
+ function openSearchModal() {
2070
+ const modal = document.getElementById('search-modal');
2071
+ if (modal) {
2072
+ modal.classList.remove('hidden');
2073
+ const input = document.getElementById('modal-search-input');
2074
+ if (input) { input.value = ''; input.focus(); }
2075
+ const results = document.getElementById('modal-search-results');
2076
+ if (results) results.innerHTML = '';
2077
+ }
2078
+ }
2079
+
2080
+ function closeSearchModal() {
2081
+ const modal = document.getElementById('search-modal');
2082
+ if (modal) modal.classList.add('hidden');
2083
+ }
2084
+
2085
+ // --- Keyboard shortcuts ---
2086
+ document.addEventListener('keydown', (e) => {
2087
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
2088
+ e.preventDefault();
2089
+ openSearchModal();
2090
+ }
2091
+ if (e.key === 'Escape') {
2092
+ closeSearchModal();
2093
+ }
2094
+ });
2095
+ `;
2096
+
2097
+ // src/dashboard/static/styles.css
2098
+ var styles_default = "/* ===== CSS VARIABLES (Dark Theme) ===== */\n:root {\n --bg: #0d1117;\n --bg-secondary: #161b22;\n --bg-tertiary: #21262d;\n --border: #30363d;\n --text: #e6edf3;\n --text-muted: #8b949e;\n --accent: #58a6ff;\n --green: #3fb950;\n --yellow: #d29922;\n --red: #f85149;\n --purple: #bc8cff;\n --radius: 8px;\n}\n\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\nbody {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n background: var(--bg);\n color: var(--text);\n min-height: 100vh;\n overflow-x: hidden;\n}\n\n.hidden { display: none !important; }\n\n/* ===== MOBILE NAV ===== */\n.mobile-nav {\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n background: var(--bg-secondary);\n border-top: 1px solid var(--border);\n display: flex;\n z-index: 100;\n padding-bottom: env(safe-area-inset-bottom);\n}\n\n.nav-btn {\n flex: 1;\n padding: 10px 4px;\n background: none;\n border: none;\n color: var(--text-muted);\n font-size: 10px;\n font-family: inherit;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 3px;\n transition: color 0.15s;\n}\n\n.nav-btn.active { color: var(--accent); }\n.nav-btn svg { width: 22px; height: 22px; }\n\n/* ===== PAGES ===== */\n.page { display: none; padding: 16px 16px 80px; }\n.page.active { display: block; }\n\n/* ===== HEADER ===== */\n.page-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 16px;\n}\n\n.page-header h1 {\n font-size: 20px;\n font-weight: 700;\n color: var(--accent);\n}\n\n/* ===== SUBJECT SWITCHER ===== */\n.subject-switcher {\n display: flex;\n gap: 6px;\n margin-bottom: 14px;\n overflow-x: auto;\n padding-bottom: 4px;\n -webkit-overflow-scrolling: touch;\n}\n\n.subject-btn {\n padding: 6px 14px;\n background: var(--bg-tertiary);\n border: 1px solid var(--border);\n border-radius: 16px;\n color: var(--text-muted);\n font-size: 13px;\n cursor: pointer;\n font-family: inherit;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.subject-btn.active { color: var(--accent); border-color: var(--accent); background: rgba(88,166,255,0.1); }\n\n/* ===== PROGRESS BAR ===== */\n.progress-bar {\n position: relative;\n height: 26px;\n background: var(--bg-tertiary);\n border-radius: 13px;\n overflow: hidden;\n margin-bottom: 16px;\n}\n\n.progress-fill {\n height: 100%;\n background: linear-gradient(90deg, var(--green), var(--accent));\n border-radius: 13px;\n transition: width 0.5s ease;\n}\n\n.progress-text {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 600;\n}\n\n/* ===== STATS GRID ===== */\n.stats-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 10px;\n margin-bottom: 20px;\n}\n\n.stat-card {\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 14px;\n text-align: center;\n}\n\n.stat-value {\n font-size: 24px;\n font-weight: 700;\n color: var(--accent);\n}\n\n.stat-value.green { color: var(--green); }\n.stat-value.yellow { color: var(--yellow); }\n.stat-value.purple { color: var(--purple); }\n\n.stat-label {\n font-size: 11px;\n color: var(--text-muted);\n margin-top: 2px;\n}\n\n/* ===== SECTION DIVIDER ===== */\n.section-divider {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--text-muted);\n margin: 16px 0 10px;\n}\n\n/* ===== PHASE TREE (Topics page) ===== */\n.phase-group { margin-bottom: 8px; }\n\n.phase-header {\n padding: 10px 14px;\n font-size: 12px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--text-muted);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n user-select: none;\n}\n\n.phase-header:hover { color: var(--text); }\n\n.phase-header .chevron {\n transition: transform 0.2s;\n font-size: 14px;\n}\n\n.phase-header.collapsed .chevron { transform: rotate(-90deg); }\n\n.phase-topics { padding: 4px 0; }\n.phase-topics.collapsed { display: none; }\n\n.topic-item {\n padding: 10px 14px 10px 20px;\n font-size: 14px;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 10px;\n color: var(--text-muted);\n border-left: 3px solid transparent;\n transition: all 0.15s;\n}\n\n.topic-item:active { background: var(--bg-tertiary); }\n.topic-item.active { background: var(--bg-tertiary); color: var(--text); border-left-color: var(--accent); }\n\n.status-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.status-dot.done { background: var(--green); }\n.status-dot.in_progress { background: var(--yellow); }\n.status-dot.todo { background: var(--bg-tertiary); border: 1.5px solid var(--text-muted); }\n\n.topic-count {\n margin-left: auto;\n font-size: 11px;\n color: var(--text-muted);\n}\n\n/* ===== BACK BUTTON ===== */\n.back-btn {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n background: none;\n border: none;\n color: var(--accent);\n font-size: 14px;\n font-family: inherit;\n cursor: pointer;\n margin-bottom: 12px;\n padding: 4px 0;\n}\n\n/* ===== TOPIC DETAIL ===== */\n.topic-title-row {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 6px;\n flex-wrap: wrap;\n}\n\n.topic-title-row h2 { font-size: 18px; font-weight: 600; }\n\n.badge {\n font-size: 10px;\n font-weight: 600;\n padding: 3px 10px;\n border-radius: 12px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.badge.todo { background: var(--bg-tertiary); color: var(--text-muted); }\n.badge.in_progress { background: rgba(210,153,34,0.15); color: var(--yellow); }\n.badge.done { background: rgba(63,185,80,0.15); color: var(--green); }\n\n.topic-desc {\n color: var(--text-muted);\n font-size: 13px;\n margin-bottom: 14px;\n line-height: 1.4;\n}\n\n/* ===== TABS ===== */\n.tabs {\n display: flex;\n gap: 4px;\n margin-bottom: 16px;\n overflow-x: auto;\n padding-bottom: 4px;\n -webkit-overflow-scrolling: touch;\n}\n\n.tab-btn {\n padding: 7px 14px;\n background: transparent;\n border: 1px solid var(--border);\n border-radius: 6px;\n color: var(--text-muted);\n cursor: pointer;\n font-size: 13px;\n font-family: inherit;\n white-space: nowrap;\n flex-shrink: 0;\n transition: all 0.15s;\n}\n\n.tab-btn:hover { color: var(--text); background: var(--bg-tertiary); }\n.tab-btn.active { color: var(--accent); border-color: var(--accent); background: rgba(88,166,255,0.1); }\n\n.tab-panel { display: none; }\n.tab-panel.active { display: block; }\n\n/* ===== Q&A CARDS ===== */\n.qa-card {\n border: 1px solid var(--border);\n border-radius: var(--radius);\n overflow: hidden;\n margin-bottom: 14px;\n}\n\n.qa-question { background: var(--bg-secondary); border-bottom: 1px solid var(--border); }\n.qa-answer { background: var(--bg-secondary); }\n.qa-answer + .qa-answer { border-top: 1px solid var(--border); }\n\n.entry-card {\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n overflow: hidden;\n margin-bottom: 14px;\n}\n\n.entry-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 14px;\n background: var(--bg-tertiary);\n font-size: 11px;\n color: var(--text-muted);\n border-bottom: 1px solid var(--border);\n}\n\n.entry-kind {\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.entry-kind.question { color: var(--accent); }\n.entry-kind.answer { color: var(--green); }\n.entry-kind.note { color: var(--purple); }\n\n.entry-body {\n padding: 14px;\n font-size: 14px;\n line-height: 1.6;\n background: var(--bg-secondary);\n}\n\n.entry-body p { margin-bottom: 10px; }\n.entry-body p:last-child { margin-bottom: 0; }\n\n.entry-body h1, .entry-body h2, .entry-body h3 {\n margin-top: 16px;\n margin-bottom: 8px;\n}\n\n.entry-body h1:first-child, .entry-body h2:first-child, .entry-body h3:first-child {\n margin-top: 0;\n}\n\n.entry-body code {\n font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;\n font-size: 13px;\n}\n\n.entry-body :not(pre) > code {\n background: var(--bg-tertiary);\n padding: 2px 5px;\n border-radius: 4px;\n font-size: 12px;\n}\n\n.entry-body pre {\n background: var(--bg);\n border: 1px solid var(--border);\n border-radius: 6px;\n padding: 12px;\n overflow-x: auto;\n margin: 8px 0;\n -webkit-overflow-scrolling: touch;\n}\n\n.entry-body pre code {\n background: none;\n padding: 0;\n font-size: 12px;\n color: var(--text);\n}\n\n.entry-body ul, .entry-body ol {\n padding-left: 24px;\n margin-bottom: 12px;\n}\n\n.entry-body li { margin-bottom: 4px; }\n\n.entry-body blockquote {\n border-left: 3px solid var(--accent);\n padding-left: 16px;\n color: var(--text-muted);\n margin: 12px 0;\n}\n\n.entry-body table {\n width: 100%;\n border-collapse: collapse;\n margin: 12px 0;\n}\n\n.entry-body th, .entry-body td {\n border: 1px solid var(--border);\n padding: 8px 12px;\n text-align: left;\n}\n\n.entry-body th {\n background: var(--bg-tertiary);\n font-weight: 600;\n}\n\n/* ===== VIZ PANEL ===== */\n.viz-selector {\n display: flex;\n gap: 6px;\n margin-bottom: 14px;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n padding-bottom: 4px;\n}\n\n.viz-select-btn {\n padding: 7px 12px;\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 6px;\n color: var(--text-muted);\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n white-space: nowrap;\n flex-shrink: 0;\n transition: all 0.15s;\n}\n\n.viz-select-btn:hover { color: var(--text); border-color: var(--text-muted); }\n.viz-select-btn.active { color: var(--accent); border-color: var(--accent); background: rgba(88,166,255,0.1); }\n\n.viz-stage {\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n overflow: hidden;\n}\n\n.viz-canvas {\n padding: 20px 12px;\n min-height: 140px;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.viz-description {\n padding: 14px;\n border-top: 1px solid var(--border);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.viz-description code {\n background: var(--bg-tertiary);\n padding: 2px 5px;\n border-radius: 4px;\n font-size: 11px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n color: var(--accent);\n}\n\n.viz-controls {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 14px;\n padding: 10px;\n border-top: 1px solid var(--border);\n background: var(--bg-tertiary);\n}\n\n.viz-controls button {\n padding: 8px 18px;\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 6px;\n color: var(--text);\n cursor: pointer;\n font-size: 13px;\n font-family: inherit;\n transition: all 0.15s;\n}\n\n.viz-controls button:hover:not(:disabled) {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.viz-controls button:disabled { opacity: 0.3; cursor: default; }\n\n.viz-step-label { font-size: 12px; color: var(--text-muted); min-width: 80px; text-align: center; }\n\n/* Viz primitives */\n.viz-box {\n padding: 10px 14px;\n border-radius: 8px;\n font-size: 12px;\n font-weight: 600;\n font-family: 'SF Mono', 'Fira Code', monospace;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n}\n\n.viz-box-label {\n font-size: 10px;\n color: var(--text-muted);\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n font-weight: 400;\n}\n\n.viz-arrow { font-size: 20px; color: var(--accent); }\n\n.box-blue { background: rgba(88,166,255,0.15); border: 1px solid var(--accent); color: var(--accent); }\n.box-green { background: rgba(63,185,80,0.15); border: 1px solid var(--green); color: var(--green); }\n.box-yellow { background: rgba(210,153,34,0.15); border: 1px solid var(--yellow); color: var(--yellow); }\n.box-purple { background: rgba(188,140,255,0.15); border: 1px solid var(--purple); color: var(--purple); }\n\n.viz-slot {\n width: 28px;\n height: 28px;\n border: 1px solid var(--border);\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 10px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n transition: all 0.3s ease;\n}\n\n.viz-slot.filled {\n background: rgba(88,166,255,0.2);\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.viz-slot.empty { color: var(--text-muted); }\n\n.viz-select-case {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 14px;\n border: 1px solid var(--border);\n border-radius: 6px;\n font-size: 12px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n transition: all 0.3s ease;\n min-width: 200px;\n}\n\n.viz-select-case.selected {\n border-color: var(--green);\n background: rgba(63,185,80,0.1);\n color: var(--green);\n}\n\n.viz-select-case.waiting { color: var(--text-muted); }\n\n.viz-flow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n/* ===== EXERCISE CARDS ===== */\n.exercise-card {\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 14px;\n margin-bottom: 10px;\n}\n\n.exercise-card.expandable { cursor: pointer; }\n.exercise-card.expandable:active { background: var(--bg-tertiary); }\n\n.exercise-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 6px;\n gap: 8px;\n}\n\n.exercise-title { font-weight: 600; font-size: 14px; }\n\n.exercise-type {\n font-size: 10px;\n padding: 3px 8px;\n border-radius: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.exercise-type.coding { background: rgba(88,166,255,0.15); color: var(--accent); }\n.exercise-type.quiz { background: rgba(188,140,255,0.15); color: var(--purple); }\n.exercise-type.project { background: rgba(210,153,34,0.15); color: var(--yellow); }\n.exercise-type.assignment { background: rgba(248,81,73,0.15); color: var(--red); }\n\n.exercise-desc {\n color: var(--text-muted);\n font-size: 13px;\n line-height: 1.5;\n margin-bottom: 10px;\n}\n\n.exercise-meta {\n display: flex;\n gap: 12px;\n font-size: 11px;\n color: var(--text-muted);\n flex-wrap: wrap;\n}\n\n.exercise-expand-icon {\n font-size: 12px;\n color: var(--text-muted);\n transition: transform 0.2s;\n flex-shrink: 0;\n}\n\n.exercise-detail {\n display: none;\n margin-top: 12px;\n padding-top: 12px;\n border-top: 1px solid var(--border);\n}\n\n.exercise-detail.open { display: block; }\n\n.exercise-detail h4 {\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--text-muted);\n margin-bottom: 8px;\n margin-top: 14px;\n}\n\n.exercise-detail h4:first-child { margin-top: 0; }\n\n.exercise-detail p, .exercise-detail li {\n font-size: 13px;\n line-height: 1.6;\n color: var(--text);\n}\n\n.exercise-detail ul { padding-left: 18px; margin-bottom: 8px; }\n.exercise-detail li { margin-bottom: 4px; }\n\n.exercise-detail pre {\n background: var(--bg);\n border: 1px solid var(--border);\n border-radius: 6px;\n padding: 12px;\n overflow-x: auto;\n margin: 8px 0;\n -webkit-overflow-scrolling: touch;\n}\n\n.exercise-detail code {\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n}\n\n.exercise-detail :not(pre) > code {\n background: var(--bg-tertiary);\n padding: 1px 5px;\n border-radius: 3px;\n color: var(--accent);\n}\n\n.exercise-detail pre code {\n background: none;\n padding: 0;\n color: var(--text);\n}\n\n/* Test cases */\n.test-case {\n background: var(--bg);\n border: 1px solid var(--border);\n border-radius: 6px;\n margin-bottom: 8px;\n overflow: hidden;\n}\n\n.test-case-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n font-size: 12px;\n font-weight: 600;\n font-family: 'SF Mono', 'Fira Code', monospace;\n background: var(--bg-tertiary);\n border-bottom: 1px solid var(--border);\n}\n\n.test-status {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.test-status.pass { background: var(--green); }\n.test-status.fail { background: var(--red); }\n.test-status.pending { background: var(--bg-tertiary); border: 1.5px solid var(--text-muted); }\n\n.test-case-body {\n padding: 10px 12px;\n font-size: 12px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n color: var(--text-muted);\n line-height: 1.5;\n}\n\n/* Quiz questions */\n.quiz-question {\n background: var(--bg);\n border: 1px solid var(--border);\n border-radius: 6px;\n padding: 14px;\n margin-bottom: 10px;\n}\n\n.quiz-question p { font-size: 14px; margin-bottom: 10px; }\n\n.quiz-option {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n margin-bottom: 4px;\n border: 1px solid var(--border);\n border-radius: 6px;\n cursor: pointer;\n font-size: 13px;\n transition: all 0.15s;\n}\n\n.quiz-option:hover { border-color: var(--accent); background: rgba(88,166,255,0.05); }\n.quiz-option.selected { border-color: var(--accent); background: rgba(88,166,255,0.1); color: var(--accent); }\n.quiz-option.correct { border-color: var(--green); background: rgba(63,185,80,0.1); color: var(--green); }\n.quiz-option.incorrect { border-color: var(--red); background: rgba(248,81,73,0.1); color: var(--red); }\n\n/* Action buttons */\n.exercise-actions {\n display: flex;\n gap: 8px;\n margin-top: 14px;\n flex-wrap: wrap;\n}\n\n.exercise-action-btn {\n padding: 10px 16px;\n border-radius: 6px;\n font-size: 13px;\n font-weight: 600;\n font-family: inherit;\n cursor: pointer;\n border: none;\n flex: 1;\n min-width: 120px;\n text-align: center;\n}\n\n.btn-primary { background: var(--accent); color: #0d1117; }\n.btn-secondary { background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text); }\n.btn-success { background: rgba(63,185,80,0.15); border: 1px solid var(--green); color: var(--green); }\n\n/* Exercise progress bar */\n.exercise-progress {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n padding: 10px 12px;\n background: var(--bg);\n border-radius: 6px;\n font-size: 12px;\n}\n\n.exercise-progress-bar {\n flex: 1;\n height: 6px;\n background: var(--bg-tertiary);\n border-radius: 3px;\n overflow: hidden;\n}\n\n.exercise-progress-fill { height: 100%; border-radius: 3px; }\n.exercise-progress-fill.green { background: var(--green); }\n.exercise-progress-fill.yellow { background: var(--yellow); }\n\n/* ===== SEARCH ===== */\n.search-bar {\n position: relative;\n margin-bottom: 16px;\n}\n\n.search-bar input {\n width: 100%;\n padding: 12px 16px;\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n color: var(--text);\n font-size: 14px;\n font-family: inherit;\n outline: none;\n}\n\n.search-bar input:focus { border-color: var(--accent); }\n.search-bar input::placeholder { color: var(--text-muted); }\n\n.search-result-item {\n padding: 12px 14px;\n cursor: pointer;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n margin-bottom: 8px;\n background: var(--bg-secondary);\n transition: background 0.1s;\n}\n\n.search-result-item:hover { background: var(--bg-tertiary); }\n\n.search-result-meta {\n font-size: 11px;\n color: var(--text-muted);\n margin-bottom: 4px;\n display: flex;\n gap: 8px;\n}\n\n.search-result-content {\n font-size: 13px;\n color: var(--text);\n line-height: 1.5;\n max-height: 60px;\n overflow: hidden;\n}\n\n.search-no-results {\n padding: 32px 20px;\n text-align: center;\n color: var(--text-muted);\n}\n\n/* Search modal (desktop) */\n.modal {\n position: fixed;\n inset: 0;\n z-index: 200;\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding-top: 15vh;\n}\n\n.modal-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(0,0,0,0.6);\n backdrop-filter: blur(4px);\n}\n\n.modal-content {\n position: relative;\n width: 600px;\n max-width: 90vw;\n max-height: 500px;\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 12px;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n box-shadow: 0 16px 48px rgba(0,0,0,0.4);\n}\n\n.modal-content input {\n width: 100%;\n padding: 16px 20px;\n background: transparent;\n border: none;\n border-bottom: 1px solid var(--border);\n color: var(--text);\n font-size: 16px;\n outline: none;\n font-family: inherit;\n}\n\n.modal-content input::placeholder { color: var(--text-muted); }\n\n.modal-results {\n overflow-y: auto;\n max-height: 400px;\n}\n\n/* ===== EMPTY STATES ===== */\n.empty-state {\n text-align: center;\n padding: 48px 16px;\n color: var(--text-muted);\n}\n\n.empty-state p { margin-bottom: 8px; }\n\n.empty-state code {\n background: var(--bg-tertiary);\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 12px;\n}\n\n/* ===== KEYBOARD SHORTCUTS ===== */\nkbd {\n background: var(--bg-tertiary);\n border: 1px solid var(--border);\n border-radius: 4px;\n padding: 2px 6px;\n font-size: 11px;\n font-family: inherit;\n color: var(--text-muted);\n}\n\n/* ===== SCROLLBAR ===== */\n::-webkit-scrollbar { width: 8px; }\n::-webkit-scrollbar-track { background: transparent; }\n::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }\n::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }\n\n/* ===== SSE STATUS ===== */\n.sse-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n display: inline-block;\n}\n\n.sse-dot.connected { background: var(--green); }\n.sse-dot.disconnected { background: var(--red); }\n\n/* ===== DESKTOP LAYOUT ===== */\n@media (min-width: 769px) {\n .mobile-nav { display: none; }\n body { display: flex; height: 100vh; overflow: hidden; }\n\n #desktop-sidebar {\n display: flex !important;\n width: 300px;\n min-width: 300px;\n background: var(--bg-secondary);\n border-right: 1px solid var(--border);\n flex-direction: column;\n overflow: hidden;\n }\n\n #desktop-sidebar .sidebar-inner {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n }\n\n #desktop-sidebar .sidebar-footer {\n padding: 12px 16px;\n border-top: 1px solid var(--border);\n font-size: 12px;\n color: var(--text-muted);\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n .page-container {\n flex: 1;\n overflow-y: auto;\n padding: 32px 48px;\n }\n\n .page { padding: 0 0 32px; }\n}\n\n@media (max-width: 768px) {\n #desktop-sidebar { display: none !important; }\n .page-container { display: contents; }\n}\n";
2099
+
2100
+ // src/dashboard/server.ts
1083
2101
  var STATIC_FILES = {
1084
- "/": { content: indexHtml, contentType: "text/html; charset=utf-8" },
1085
- "/index.html": { content: indexHtml, contentType: "text/html; charset=utf-8" },
1086
- "/app.js": { content: appJs, contentType: "application/javascript; charset=utf-8" },
1087
- "/styles.css": { content: stylesCss, contentType: "text/css; charset=utf-8" }
2102
+ "/": { content: static_default, contentType: "text/html; charset=utf-8" },
2103
+ "/index.html": { content: static_default, contentType: "text/html; charset=utf-8" },
2104
+ "/app.js": { content: app_default, contentType: "application/javascript; charset=utf-8" },
2105
+ "/styles.css": { content: styles_default, contentType: "text/css; charset=utf-8" }
1088
2106
  };
1089
2107
  var DashboardServer = class {
1090
2108
  constructor(curriculumSvc2, qaSvc2, vizSvc2, exerciseSvc2, port2) {