@bis-code/study-dash 0.2.1 → 0.3.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "study-dash",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Structured learning on any subject with dashboard, Q&A logging, visualizations, exercises",
5
5
  "author": {
6
6
  "name": "Ioan-Sorin Baicoianu",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bis-code/study-dash",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Claude Code plugin for structured learning on any subject — dashboard, Q&A logging, visualizations, exercises",
5
5
  "license": "MIT",
6
6
  "author": "Ioan-Sorin Baicoianu <baicoianuioansorin@gmail.com>",
@@ -103,6 +103,17 @@ CREATE TABLE IF NOT EXISTS exercise_results (
103
103
 
104
104
  CREATE INDEX IF NOT EXISTS idx_results_exercise ON exercise_results(exercise_id);
105
105
 
106
+ CREATE TABLE IF NOT EXISTS resources (
107
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
108
+ topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
109
+ title TEXT NOT NULL DEFAULT '',
110
+ url TEXT NOT NULL DEFAULT '',
111
+ source TEXT NOT NULL DEFAULT 'manual'
112
+ CHECK (source IN ('manual','auto','import')),
113
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
114
+ );
115
+ CREATE INDEX IF NOT EXISTS idx_resources_topic ON resources(topic_id);
116
+
106
117
  CREATE TABLE IF NOT EXISTS settings (
107
118
  key TEXT PRIMARY KEY,
108
119
  value TEXT NOT NULL DEFAULT ''
@@ -450,8 +461,8 @@ ${description}`;
450
461
  });
451
462
  stdout = result.stdout;
452
463
  stderr = result.stderr;
453
- } catch (err5) {
454
- const execErr = err5;
464
+ } catch (err6) {
465
+ const execErr = err6;
455
466
  stdout = execErr.stdout ?? "";
456
467
  stderr = execErr.stderr ?? "";
457
468
  exitCode = execErr.code ?? 1;
@@ -545,6 +556,39 @@ ${description}`;
545
556
  }
546
557
  };
547
558
 
559
+ // src/services/resources.ts
560
+ var ResourceService = class {
561
+ constructor(db2) {
562
+ this.db = db2;
563
+ }
564
+ addResource(topicId, title, url, source = "manual") {
565
+ const result = this.db.raw.prepare(
566
+ "INSERT INTO resources (topic_id, title, url, source) VALUES (?, ?, ?, ?)"
567
+ ).run(topicId, title, url, source);
568
+ return this.db.raw.prepare("SELECT * FROM resources WHERE id = ?").get(result.lastInsertRowid);
569
+ }
570
+ listForTopic(topicId) {
571
+ return this.db.raw.prepare(
572
+ "SELECT * FROM resources WHERE topic_id = ? ORDER BY created_at ASC, id ASC"
573
+ ).all(topicId);
574
+ }
575
+ importResources(resources) {
576
+ const insert = this.db.raw.prepare(
577
+ "INSERT INTO resources (topic_id, title, url, source) VALUES (?, ?, ?, ?)"
578
+ );
579
+ const tx = this.db.raw.transaction((items) => {
580
+ for (const r of items) {
581
+ insert.run(r.topic_id, r.title, r.url, "import");
582
+ }
583
+ return items.length;
584
+ });
585
+ return tx(resources);
586
+ }
587
+ deleteResource(id) {
588
+ this.db.raw.prepare("DELETE FROM resources WHERE id = ?").run(id);
589
+ }
590
+ };
591
+
548
592
  // src/tools/curriculum.ts
549
593
  import { z } from "zod";
550
594
  function getSession(sessions2, sessionId) {
@@ -917,6 +961,64 @@ function registerExerciseTools(server2, svc, sessions2, notify2) {
917
961
  );
918
962
  }
919
963
 
964
+ // src/tools/resources.ts
965
+ import { z as z5 } from "zod";
966
+ function getSession5(sessions2, sessionId) {
967
+ const key = sessionId || "_default";
968
+ if (!sessions2.has(key)) {
969
+ sessions2.set(key, { subjectId: null, topicId: null });
970
+ }
971
+ return sessions2.get(key);
972
+ }
973
+ function err5(text) {
974
+ return { content: [{ type: "text", text }], isError: true };
975
+ }
976
+ function ok5(text) {
977
+ return { content: [{ type: "text", text }] };
978
+ }
979
+ function registerResourceTools(server2, svc, sessions2, notify2) {
980
+ server2.tool(
981
+ "learn_add_resource",
982
+ "Add a reference link to the active topic (or a specific topic by ID)",
983
+ {
984
+ title: z5.string().describe("Resource title"),
985
+ url: z5.string().describe("Resource URL"),
986
+ topic_id: z5.number().optional().describe("Topic ID (defaults to active topic)"),
987
+ session_id: z5.string().optional()
988
+ },
989
+ async ({ title, url, topic_id, session_id }) => {
990
+ const tid = topic_id ?? getSession5(sessions2, session_id).topicId;
991
+ if (tid === null) {
992
+ return err5("No active topic. Use learn_set_topic first or provide topic_id.");
993
+ }
994
+ const resource = svc.addResource(tid, title, url, "manual");
995
+ notify2();
996
+ return ok5(`Added resource "${resource.title}" (id=${resource.id}) to topic ${tid}`);
997
+ }
998
+ );
999
+ server2.tool(
1000
+ "learn_import_resources",
1001
+ "Bulk import resource links from a JSON array of {topic_id, title, url} objects",
1002
+ {
1003
+ resources_json: z5.string().describe("JSON array of {topic_id: number, title: string, url: string}")
1004
+ },
1005
+ async ({ resources_json }) => {
1006
+ let resources;
1007
+ try {
1008
+ resources = JSON.parse(resources_json);
1009
+ } catch {
1010
+ return err5("Invalid JSON");
1011
+ }
1012
+ if (!Array.isArray(resources)) {
1013
+ return err5("Expected a JSON array");
1014
+ }
1015
+ const count = svc.importResources(resources);
1016
+ notify2();
1017
+ return ok5(`Imported ${count} resources`);
1018
+ }
1019
+ );
1020
+ }
1021
+
920
1022
  // src/dashboard/server.ts
921
1023
  import http from "node:http";
922
1024
 
@@ -935,7 +1037,7 @@ function parseBody(req) {
935
1037
  req.on("end", () => {
936
1038
  try {
937
1039
  resolve(JSON.parse(Buffer.concat(chunks).toString()));
938
- } catch (err5) {
1040
+ } catch (err6) {
939
1041
  reject(new Error("Invalid JSON body"));
940
1042
  }
941
1043
  });
@@ -970,7 +1072,7 @@ function handlePhases(curriculumSvc2) {
970
1072
  writeJSON(res, phases);
971
1073
  };
972
1074
  }
973
- function handleTopic(curriculumSvc2, qaSvc2) {
1075
+ function handleTopic(curriculumSvc2, qaSvc2, resourceSvc2) {
974
1076
  return (req, res) => {
975
1077
  const id = extractId(req.url ?? "", "/api/topics/");
976
1078
  if (id === null) {
@@ -983,7 +1085,19 @@ function handleTopic(curriculumSvc2, qaSvc2) {
983
1085
  return;
984
1086
  }
985
1087
  const entries = qaSvc2.listEntries(id);
986
- writeJSON(res, { ...topic, entries });
1088
+ const resources = resourceSvc2.listForTopic(id);
1089
+ writeJSON(res, { ...topic, entries, resources });
1090
+ };
1091
+ }
1092
+ function handleTopicResources(resourceSvc2) {
1093
+ return (req, res) => {
1094
+ const id = extractId(req.url ?? "", "/api/topics/");
1095
+ if (id === null) {
1096
+ writeError(res, 400, "Invalid topic ID");
1097
+ return;
1098
+ }
1099
+ const resources = resourceSvc2.listForTopic(id);
1100
+ writeJSON(res, resources);
987
1101
  };
988
1102
  }
989
1103
  function handleTopicViz(vizSvc2) {
@@ -1018,8 +1132,8 @@ function handleRunTests(exerciseSvc2) {
1018
1132
  try {
1019
1133
  const results = await exerciseSvc2.runTests(id);
1020
1134
  writeJSON(res, results);
1021
- } catch (err5) {
1022
- const msg = err5 instanceof Error ? err5.message : String(err5);
1135
+ } catch (err6) {
1136
+ const msg = err6 instanceof Error ? err6.message : String(err6);
1023
1137
  writeError(res, 500, msg);
1024
1138
  }
1025
1139
  };
@@ -1039,8 +1153,8 @@ function handleSubmitQuiz(exerciseSvc2) {
1039
1153
  }
1040
1154
  const result = exerciseSvc2.submitQuiz(id, body.answers);
1041
1155
  writeJSON(res, result);
1042
- } catch (err5) {
1043
- const msg = err5 instanceof Error ? err5.message : String(err5);
1156
+ } catch (err6) {
1157
+ const msg = err6 instanceof Error ? err6.message : String(err6);
1044
1158
  writeError(res, 500, msg);
1045
1159
  }
1046
1160
  };
@@ -1056,8 +1170,8 @@ function handleSearch(qaSvc2) {
1056
1170
  try {
1057
1171
  const results = qaSvc2.search(query);
1058
1172
  writeJSON(res, results);
1059
- } catch (err5) {
1060
- const msg = err5 instanceof Error ? err5.message : String(err5);
1173
+ } catch (err6) {
1174
+ const msg = err6 instanceof Error ? err6.message : String(err6);
1061
1175
  writeError(res, 500, msg);
1062
1176
  }
1063
1177
  };
@@ -1217,6 +1331,7 @@ const state = {
1217
1331
  topicData: null,
1218
1332
  topicViz: [],
1219
1333
  topicExercises: [],
1334
+ topicResources: [],
1220
1335
  searchTimeout: null,
1221
1336
  vizIndex: 0,
1222
1337
  vizStep: 0,
@@ -1562,21 +1677,24 @@ async function selectTopic(id) {
1562
1677
  state.activeTopic = id;
1563
1678
  state.activeTab = 'qa';
1564
1679
 
1565
- // Fetch topic detail, viz, and exercises in parallel
1680
+ // Fetch topic detail, viz, exercises, and resources in parallel
1566
1681
  try {
1567
- const [topicData, viz, exercises] = await Promise.all([
1682
+ const [topicData, viz, exercises, resources] = await Promise.all([
1568
1683
  api(\`/api/topics/\${id}\`),
1569
1684
  api(\`/api/topics/\${id}/viz\`).catch(() => []),
1570
1685
  api(\`/api/topics/\${id}/exercises\`).catch(() => []),
1686
+ api(\`/api/topics/\${id}/resources\`).catch(() => []),
1571
1687
  ]);
1572
1688
 
1573
1689
  state.topicData = topicData;
1574
1690
  state.topicViz = viz || [];
1575
1691
  state.topicExercises = exercises || [];
1692
+ state.topicResources = resources || [];
1576
1693
  } catch {
1577
1694
  state.topicData = null;
1578
1695
  state.topicViz = [];
1579
1696
  state.topicExercises = [];
1697
+ state.topicResources = [];
1580
1698
  }
1581
1699
 
1582
1700
  // Update sidebar active state
@@ -1890,8 +2008,10 @@ function renderExercisesTab() {
1890
2008
  }
1891
2009
 
1892
2010
  function toggleExercise(index) {
1893
- const detail = document.getElementById(\`exercise-detail-\${index}\`);
2011
+ const detail = document.getElementById('exercise-detail-' + index);
2012
+ const card = detail ? detail.closest('.exercise-card') : null;
1894
2013
  if (detail) detail.classList.toggle('open');
2014
+ if (card) card.classList.toggle('open');
1895
2015
  }
1896
2016
 
1897
2017
  function selectQuizOption(el) {
@@ -1978,29 +2098,22 @@ function renderResourcesTab() {
1978
2098
  const container = document.getElementById('tab-resources');
1979
2099
  if (!container) return;
1980
2100
 
1981
- const data = state.topicData;
1982
- if (!data) {
1983
- container.innerHTML = \`<div class="empty-state"><p>No resources available</p></div>\`;
2101
+ const resources = state.topicResources || [];
2102
+
2103
+ if (resources.length === 0) {
2104
+ container.innerHTML = '<div class="empty-state"><p>No resources yet</p><p class="text-muted">Ask Claude to add reference links for this topic</p></div>';
1984
2105
  return;
1985
2106
  }
1986
2107
 
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>\`;
2108
+ let html = '<div class="resources-list">';
2109
+ for (const r of resources) {
2110
+ html += '<a href="' + escapeHtml(r.url) + '" target="_blank" rel="noopener" class="resource-card">' +
2111
+ '<span class="resource-title">' + escapeHtml(r.title) + '</span>' +
2112
+ '<span class="resource-url">' + escapeHtml(r.url) + '</span>' +
2113
+ '</a>';
2114
+ }
2115
+ html += '</div>';
2116
+ container.innerHTML = html;
2004
2117
  }
2005
2118
 
2006
2119
  // --- Navigation ---
@@ -2095,7 +2208,7 @@ document.addEventListener('keydown', (e) => {
2095
2208
  `;
2096
2209
 
2097
2210
  // 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";
2211
+ 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\n/* ===== RESOURCES ===== */\n.resources-list {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.resource-card {\n display: flex;\n flex-direction: column;\n padding: 0.75rem 1rem;\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 8px;\n text-decoration: none;\n color: var(--text);\n transition: border-color 0.15s, background 0.15s;\n}\n\n.resource-card:hover {\n border-color: var(--accent);\n background: var(--bg-tertiary);\n}\n\n.resource-title {\n font-weight: 500;\n}\n\n.resource-url {\n font-size: 0.8rem;\n color: var(--text-muted);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* ===== EXERCISE EXPAND UX ===== */\n.exercise-header {\n cursor: pointer;\n}\n\n.exercise-header:hover {\n background: var(--bg-tertiary);\n border-radius: var(--radius);\n}\n\n.exercise-card.open .exercise-expand-icon {\n transform: rotate(180deg);\n}\n";
2099
2212
 
2100
2213
  // src/dashboard/server.ts
2101
2214
  var STATIC_FILES = {
@@ -2105,11 +2218,12 @@ var STATIC_FILES = {
2105
2218
  "/styles.css": { content: styles_default, contentType: "text/css; charset=utf-8" }
2106
2219
  };
2107
2220
  var DashboardServer = class {
2108
- constructor(curriculumSvc2, qaSvc2, vizSvc2, exerciseSvc2, port2) {
2221
+ constructor(curriculumSvc2, qaSvc2, vizSvc2, exerciseSvc2, resourceSvc2, port2) {
2109
2222
  this.curriculumSvc = curriculumSvc2;
2110
2223
  this.qaSvc = qaSvc2;
2111
2224
  this.vizSvc = vizSvc2;
2112
2225
  this.exerciseSvc = exerciseSvc2;
2226
+ this.resourceSvc = resourceSvc2;
2113
2227
  this.port = port2;
2114
2228
  }
2115
2229
  sseClients = /* @__PURE__ */ new Set();
@@ -2193,8 +2307,12 @@ var DashboardServer = class {
2193
2307
  handleTopicExercises(this.exerciseSvc)(req, res);
2194
2308
  return;
2195
2309
  }
2310
+ if (method === "GET" && /^\/api\/topics\/\d+\/resources$/.test(path)) {
2311
+ handleTopicResources(this.resourceSvc)(req, res);
2312
+ return;
2313
+ }
2196
2314
  if (method === "GET" && /^\/api\/topics\/\d+$/.test(path)) {
2197
- handleTopic(this.curriculumSvc, this.qaSvc)(req, res);
2315
+ handleTopic(this.curriculumSvc, this.qaSvc, this.resourceSvc)(req, res);
2198
2316
  return;
2199
2317
  }
2200
2318
  if (method === "POST" && /^\/api\/exercises\/\d+\/run$/.test(path)) {
@@ -2238,21 +2356,23 @@ var curriculumSvc = new CurriculumService(db);
2238
2356
  var qaSvc = new QAService(db);
2239
2357
  var vizSvc = new VizService(db);
2240
2358
  var exerciseSvc = new ExerciseService(db, fileStore);
2359
+ var resourceSvc = new ResourceService(db);
2241
2360
  var port = Number(db.getSetting("dashboard_port") ?? "19282");
2242
- var dashboard = new DashboardServer(curriculumSvc, qaSvc, vizSvc, exerciseSvc, port);
2361
+ var dashboard = new DashboardServer(curriculumSvc, qaSvc, vizSvc, exerciseSvc, resourceSvc, port);
2243
2362
  var notify = () => dashboard.notify();
2244
2363
  var server = new McpServer({ name: "study-dash", version: "0.1.0" });
2245
2364
  registerCurriculumTools(server, curriculumSvc, sessions, notify);
2246
2365
  registerQATools(server, qaSvc, sessions, notify);
2247
2366
  registerVizTools(server, vizSvc, sessions, notify);
2248
2367
  registerExerciseTools(server, exerciseSvc, sessions, notify);
2368
+ registerResourceTools(server, resourceSvc, sessions, notify);
2249
2369
  dashboard.start();
2250
2370
  async function run() {
2251
2371
  const transport = new StdioServerTransport();
2252
2372
  await server.connect(transport);
2253
2373
  console.error(`study-dash MCP server running, dashboard at http://127.0.0.1:${port}`);
2254
2374
  }
2255
- run().catch((err5) => {
2256
- console.error("Fatal:", err5);
2375
+ run().catch((err6) => {
2376
+ console.error("Fatal:", err6);
2257
2377
  process.exit(1);
2258
2378
  });
@@ -3,12 +3,14 @@ import type { CurriculumService } from '../services/curriculum.js';
3
3
  import type { QAService } from '../services/qa.js';
4
4
  import type { VizService } from '../services/viz.js';
5
5
  import type { ExerciseService } from '../services/exercises.js';
6
+ import type { ResourceService } from '../services/resources.js';
6
7
  export declare function writeJSON(res: ServerResponse, data: unknown, status?: number): void;
7
8
  export declare function parseBody(req: IncomingMessage): Promise<unknown>;
8
9
  export declare function extractId(url: string, prefix: string): number | null;
9
10
  export declare function handleSubjects(curriculumSvc: CurriculumService): (_req: IncomingMessage, res: ServerResponse) => void;
10
11
  export declare function handlePhases(curriculumSvc: CurriculumService): (req: IncomingMessage, res: ServerResponse) => void;
11
- export declare function handleTopic(curriculumSvc: CurriculumService, qaSvc: QAService): (req: IncomingMessage, res: ServerResponse) => void;
12
+ export declare function handleTopic(curriculumSvc: CurriculumService, qaSvc: QAService, resourceSvc: ResourceService): (req: IncomingMessage, res: ServerResponse) => void;
13
+ export declare function handleTopicResources(resourceSvc: ResourceService): (req: IncomingMessage, res: ServerResponse) => void;
12
14
  export declare function handleTopicViz(vizSvc: VizService): (req: IncomingMessage, res: ServerResponse) => void;
13
15
  export declare function handleTopicExercises(exerciseSvc: ExerciseService): (req: IncomingMessage, res: ServerResponse) => void;
14
16
  export declare function handleRunTests(exerciseSvc: ExerciseService): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -52,7 +52,7 @@ export function handlePhases(curriculumSvc) {
52
52
  writeJSON(res, phases);
53
53
  };
54
54
  }
55
- export function handleTopic(curriculumSvc, qaSvc) {
55
+ export function handleTopic(curriculumSvc, qaSvc, resourceSvc) {
56
56
  return (req, res) => {
57
57
  const id = extractId(req.url ?? '', '/api/topics/');
58
58
  if (id === null) {
@@ -65,7 +65,19 @@ export function handleTopic(curriculumSvc, qaSvc) {
65
65
  return;
66
66
  }
67
67
  const entries = qaSvc.listEntries(id);
68
- writeJSON(res, { ...topic, entries });
68
+ const resources = resourceSvc.listForTopic(id);
69
+ writeJSON(res, { ...topic, entries, resources });
70
+ };
71
+ }
72
+ export function handleTopicResources(resourceSvc) {
73
+ return (req, res) => {
74
+ const id = extractId(req.url ?? '', '/api/topics/');
75
+ if (id === null) {
76
+ writeError(res, 400, 'Invalid topic ID');
77
+ return;
78
+ }
79
+ const resources = resourceSvc.listForTopic(id);
80
+ writeJSON(res, resources);
69
81
  };
70
82
  }
71
83
  export function handleTopicViz(vizSvc) {
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/dashboard/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/dashboard/api.ts"],"names":[],"mappings":"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"}
@@ -2,15 +2,17 @@ import type { CurriculumService } from '../services/curriculum.js';
2
2
  import type { QAService } from '../services/qa.js';
3
3
  import type { VizService } from '../services/viz.js';
4
4
  import type { ExerciseService } from '../services/exercises.js';
5
+ import type { ResourceService } from '../services/resources.js';
5
6
  export declare class DashboardServer {
6
7
  private curriculumSvc;
7
8
  private qaSvc;
8
9
  private vizSvc;
9
10
  private exerciseSvc;
11
+ private resourceSvc;
10
12
  private port;
11
13
  private sseClients;
12
14
  private httpServer;
13
- constructor(curriculumSvc: CurriculumService, qaSvc: QAService, vizSvc: VizService, exerciseSvc: ExerciseService, port: number);
15
+ constructor(curriculumSvc: CurriculumService, qaSvc: QAService, vizSvc: VizService, exerciseSvc: ExerciseService, resourceSvc: ResourceService, port: number);
14
16
  start(): void;
15
17
  stop(): void;
16
18
  notify(): void;
@@ -1,5 +1,5 @@
1
1
  import http from 'node:http';
2
- import { handleSubjects, handlePhases, handleTopic, handleTopicViz, handleTopicExercises, handleRunTests, handleSubmitQuiz, handleSearch, writeJSON, } from './api.js';
2
+ import { handleSubjects, handlePhases, handleTopic, handleTopicViz, handleTopicExercises, handleTopicResources, handleRunTests, handleSubmitQuiz, handleSearch, writeJSON, } from './api.js';
3
3
  // ── Embedded static content ──
4
4
  // esbuild --loader:.html=text inlines these as strings at bundle time.
5
5
  // @ts-ignore — esbuild text loader
@@ -19,14 +19,16 @@ export class DashboardServer {
19
19
  qaSvc;
20
20
  vizSvc;
21
21
  exerciseSvc;
22
+ resourceSvc;
22
23
  port;
23
24
  sseClients = new Set();
24
25
  httpServer = null;
25
- constructor(curriculumSvc, qaSvc, vizSvc, exerciseSvc, port) {
26
+ constructor(curriculumSvc, qaSvc, vizSvc, exerciseSvc, resourceSvc, port) {
26
27
  this.curriculumSvc = curriculumSvc;
27
28
  this.qaSvc = qaSvc;
28
29
  this.vizSvc = vizSvc;
29
30
  this.exerciseSvc = exerciseSvc;
31
+ this.resourceSvc = resourceSvc;
30
32
  this.port = port;
31
33
  }
32
34
  start() {
@@ -114,9 +116,14 @@ export class DashboardServer {
114
116
  handleTopicExercises(this.exerciseSvc)(req, res);
115
117
  return;
116
118
  }
119
+ // GET /api/topics/:id/resources
120
+ if (method === 'GET' && /^\/api\/topics\/\d+\/resources$/.test(path)) {
121
+ handleTopicResources(this.resourceSvc)(req, res);
122
+ return;
123
+ }
117
124
  // GET /api/topics/:id
118
125
  if (method === 'GET' && /^\/api\/topics\/\d+$/.test(path)) {
119
- handleTopic(this.curriculumSvc, this.qaSvc)(req, res);
126
+ handleTopic(this.curriculumSvc, this.qaSvc, this.resourceSvc)(req, res);
120
127
  return;
121
128
  }
122
129
  // POST /api/exercises/:id/run
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"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"}
@@ -7,10 +7,12 @@ import { CurriculumService } from './services/curriculum.js';
7
7
  import { QAService } from './services/qa.js';
8
8
  import { VizService } from './services/viz.js';
9
9
  import { ExerciseService } from './services/exercises.js';
10
+ import { ResourceService } from './services/resources.js';
10
11
  import { registerCurriculumTools } from './tools/curriculum.js';
11
12
  import { registerQATools } from './tools/qa.js';
12
13
  import { registerVizTools } from './tools/viz.js';
13
14
  import { registerExerciseTools } from './tools/exercises.js';
15
+ import { registerResourceTools } from './tools/resources.js';
14
16
  import { DashboardServer } from './dashboard/server.js';
15
17
  const fileStore = new FileStore();
16
18
  const db = new Database(fileStore.dbPath);
@@ -19,14 +21,16 @@ const curriculumSvc = new CurriculumService(db);
19
21
  const qaSvc = new QAService(db);
20
22
  const vizSvc = new VizService(db);
21
23
  const exerciseSvc = new ExerciseService(db, fileStore);
24
+ const resourceSvc = new ResourceService(db);
22
25
  const port = Number(db.getSetting('dashboard_port') ?? '19282');
23
- const dashboard = new DashboardServer(curriculumSvc, qaSvc, vizSvc, exerciseSvc, port);
26
+ const dashboard = new DashboardServer(curriculumSvc, qaSvc, vizSvc, exerciseSvc, resourceSvc, port);
24
27
  const notify = () => dashboard.notify();
25
28
  const server = new McpServer({ name: 'study-dash', version: '0.1.0' });
26
29
  registerCurriculumTools(server, curriculumSvc, sessions, notify);
27
30
  registerQATools(server, qaSvc, sessions, notify);
28
31
  registerVizTools(server, vizSvc, sessions, notify);
29
32
  registerExerciseTools(server, exerciseSvc, sessions, notify);
33
+ registerResourceTools(server, resourceSvc, sessions, notify);
30
34
  dashboard.start();
31
35
  async function run() {
32
36
  const transport = new StdioServerTransport();
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;AAClC,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAC1C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEjD,MAAM,aAAa,GAAG,IAAI,iBAAiB,CAAC,EAAE,CAAC,CAAC;AAChD,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,EAAE,CAAC,CAAC;AAChC,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;AAClC,MAAM,WAAW,GAAG,IAAI,eAAe,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;AAEvD,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC,IAAI,OAAO,CAAC,CAAC;AAChE,MAAM,SAAS,GAAG,IAAI,eAAe,CAAC,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;AACvF,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;AAExC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AAEvE,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AACjE,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AACjD,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AACnD,qBAAqB,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AAE7D,SAAS,CAAC,KAAK,EAAE,CAAC;AAElB,KAAK,UAAU,GAAG;IAChB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,gEAAgE,IAAI,EAAE,CAAC,CAAC;AACxF,CAAC;AAED,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IAClB,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { Database } from '../storage/db.js';
2
+ import type { Resource } from '../types.js';
3
+ interface ImportResourceInput {
4
+ topic_id: number;
5
+ title: string;
6
+ url: string;
7
+ }
8
+ export declare class ResourceService {
9
+ private db;
10
+ constructor(db: Database);
11
+ addResource(topicId: number, title: string, url: string, source?: string): Resource;
12
+ listForTopic(topicId: number): Resource[];
13
+ importResources(resources: ImportResourceInput[]): number;
14
+ deleteResource(id: number): void;
15
+ }
16
+ export {};
@@ -0,0 +1,27 @@
1
+ export class ResourceService {
2
+ db;
3
+ constructor(db) {
4
+ this.db = db;
5
+ }
6
+ addResource(topicId, title, url, source = 'manual') {
7
+ const result = this.db.raw.prepare('INSERT INTO resources (topic_id, title, url, source) VALUES (?, ?, ?, ?)').run(topicId, title, url, source);
8
+ return this.db.raw.prepare('SELECT * FROM resources WHERE id = ?').get(result.lastInsertRowid);
9
+ }
10
+ listForTopic(topicId) {
11
+ return this.db.raw.prepare('SELECT * FROM resources WHERE topic_id = ? ORDER BY created_at ASC, id ASC').all(topicId);
12
+ }
13
+ importResources(resources) {
14
+ const insert = this.db.raw.prepare('INSERT INTO resources (topic_id, title, url, source) VALUES (?, ?, ?, ?)');
15
+ const tx = this.db.raw.transaction((items) => {
16
+ for (const r of items) {
17
+ insert.run(r.topic_id, r.title, r.url, 'import');
18
+ }
19
+ return items.length;
20
+ });
21
+ return tx(resources);
22
+ }
23
+ deleteResource(id) {
24
+ this.db.raw.prepare('DELETE FROM resources WHERE id = ?').run(id);
25
+ }
26
+ }
27
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/services/resources.ts"],"names":[],"mappings":"AASA,MAAM,OAAO,eAAe;IACN;IAApB,YAAoB,EAAY;QAAZ,OAAE,GAAF,EAAE,CAAU;IAAG,CAAC;IAEpC,WAAW,CAAC,OAAe,EAAE,KAAa,EAAE,GAAW,EAAE,SAAiB,QAAQ;QAChF,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAChC,0EAA0E,CAC3E,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAa,CAAC;IAC7G,CAAC;IAED,YAAY,CAAC,OAAe;QAC1B,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CACxB,4EAA4E,CAC7E,CAAC,GAAG,CAAC,OAAO,CAAe,CAAC;IAC/B,CAAC;IAED,eAAe,CAAC,SAAgC;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAChC,0EAA0E,CAC3E,CAAC;QACF,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,KAA4B,EAAE,EAAE;YAClE,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YACnD,CAAC;YACD,OAAO,KAAK,CAAC,MAAM,CAAC;QACtB,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC;IACvB,CAAC;IAED,cAAc,CAAC,EAAU;QACvB,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;CACF"}
@@ -1,3 +1,3 @@
1
- export declare const schema = "\nPRAGMA foreign_keys=ON;\n\nCREATE TABLE IF NOT EXISTS subjects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n language TEXT NOT NULL DEFAULT '',\n source TEXT NOT NULL DEFAULT 'manual'\n CHECK (source IN ('manual','roadmap','pdf')),\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS phases (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n subject_id INTEGER NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT NOT NULL DEFAULT '',\n sort_order INTEGER NOT NULL DEFAULT 0\n);\n\nCREATE INDEX IF NOT EXISTS idx_phases_subject ON phases(subject_id);\n\nCREATE TABLE IF NOT EXISTS topics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n phase_id INTEGER NOT NULL REFERENCES phases(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT NOT NULL DEFAULT '',\n sort_order INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK (status IN ('todo','in_progress','done')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_topics_phase ON topics(phase_id);\nCREATE INDEX IF NOT EXISTS idx_topics_status ON topics(status);\n\nCREATE TABLE IF NOT EXISTS entries (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n kind TEXT NOT NULL CHECK (kind IN ('question','answer','note')),\n content TEXT NOT NULL DEFAULT '',\n session_id TEXT NOT NULL DEFAULT '',\n question_id INTEGER REFERENCES entries(id) ON DELETE SET NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_entries_topic ON entries(topic_id);\nCREATE INDEX IF NOT EXISTS idx_entries_session ON entries(session_id);\n\nCREATE TABLE IF NOT EXISTS visualizations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n title TEXT NOT NULL DEFAULT '',\n steps_json TEXT NOT NULL DEFAULT '[]',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_viz_topic ON visualizations(topic_id);\n\nCREATE TABLE IF NOT EXISTS exercises (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n title TEXT NOT NULL DEFAULT '',\n type TEXT NOT NULL DEFAULT 'coding'\n CHECK (type IN ('coding','quiz','project','assignment')),\n description TEXT NOT NULL DEFAULT '',\n difficulty TEXT NOT NULL DEFAULT 'medium'\n CHECK (difficulty IN ('easy','medium','hard')),\n est_minutes INTEGER NOT NULL DEFAULT 0,\n source TEXT NOT NULL DEFAULT 'ai'\n CHECK (source IN ('ai','pdf_import')),\n starter_code TEXT NOT NULL DEFAULT '',\n test_content TEXT NOT NULL DEFAULT '',\n quiz_json TEXT NOT NULL DEFAULT '{}',\n file_path TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending','in_progress','passed','failed')),\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_exercises_topic ON exercises(topic_id);\nCREATE INDEX IF NOT EXISTS idx_exercises_status ON exercises(status);\n\nCREATE TABLE IF NOT EXISTS exercise_results (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n exercise_id INTEGER NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,\n test_name TEXT NOT NULL DEFAULT '',\n passed INTEGER NOT NULL DEFAULT 0,\n output TEXT NOT NULL DEFAULT '',\n ran_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_results_exercise ON exercise_results(exercise_id);\n\nCREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL DEFAULT ''\n);\n\n-- FTS5 virtual table for full-text search over entries\nCREATE VIRTUAL TABLE IF NOT EXISTS entries_fts\n USING fts5(content, content='entries', content_rowid='id');\n\n-- Sync triggers: keep entries_fts up to date with entries\nCREATE TRIGGER IF NOT EXISTS entries_ai\n AFTER INSERT ON entries BEGIN\n INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\nCREATE TRIGGER IF NOT EXISTS entries_ad\n AFTER DELETE ON entries BEGIN\n INSERT INTO entries_fts(entries_fts, rowid, content)\n VALUES ('delete', old.id, old.content);\n END;\n\nCREATE TRIGGER IF NOT EXISTS entries_au\n AFTER UPDATE ON entries BEGIN\n INSERT INTO entries_fts(entries_fts, rowid, content)\n VALUES ('delete', old.id, old.content);\n INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);\n END;\n";
1
+ export declare const schema = "\nPRAGMA foreign_keys=ON;\n\nCREATE TABLE IF NOT EXISTS subjects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n language TEXT NOT NULL DEFAULT '',\n source TEXT NOT NULL DEFAULT 'manual'\n CHECK (source IN ('manual','roadmap','pdf')),\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS phases (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n subject_id INTEGER NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT NOT NULL DEFAULT '',\n sort_order INTEGER NOT NULL DEFAULT 0\n);\n\nCREATE INDEX IF NOT EXISTS idx_phases_subject ON phases(subject_id);\n\nCREATE TABLE IF NOT EXISTS topics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n phase_id INTEGER NOT NULL REFERENCES phases(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT NOT NULL DEFAULT '',\n sort_order INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK (status IN ('todo','in_progress','done')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_topics_phase ON topics(phase_id);\nCREATE INDEX IF NOT EXISTS idx_topics_status ON topics(status);\n\nCREATE TABLE IF NOT EXISTS entries (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n kind TEXT NOT NULL CHECK (kind IN ('question','answer','note')),\n content TEXT NOT NULL DEFAULT '',\n session_id TEXT NOT NULL DEFAULT '',\n question_id INTEGER REFERENCES entries(id) ON DELETE SET NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_entries_topic ON entries(topic_id);\nCREATE INDEX IF NOT EXISTS idx_entries_session ON entries(session_id);\n\nCREATE TABLE IF NOT EXISTS visualizations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n title TEXT NOT NULL DEFAULT '',\n steps_json TEXT NOT NULL DEFAULT '[]',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_viz_topic ON visualizations(topic_id);\n\nCREATE TABLE IF NOT EXISTS exercises (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n title TEXT NOT NULL DEFAULT '',\n type TEXT NOT NULL DEFAULT 'coding'\n CHECK (type IN ('coding','quiz','project','assignment')),\n description TEXT NOT NULL DEFAULT '',\n difficulty TEXT NOT NULL DEFAULT 'medium'\n CHECK (difficulty IN ('easy','medium','hard')),\n est_minutes INTEGER NOT NULL DEFAULT 0,\n source TEXT NOT NULL DEFAULT 'ai'\n CHECK (source IN ('ai','pdf_import')),\n starter_code TEXT NOT NULL DEFAULT '',\n test_content TEXT NOT NULL DEFAULT '',\n quiz_json TEXT NOT NULL DEFAULT '{}',\n file_path TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending','in_progress','passed','failed')),\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_exercises_topic ON exercises(topic_id);\nCREATE INDEX IF NOT EXISTS idx_exercises_status ON exercises(status);\n\nCREATE TABLE IF NOT EXISTS exercise_results (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n exercise_id INTEGER NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,\n test_name TEXT NOT NULL DEFAULT '',\n passed INTEGER NOT NULL DEFAULT 0,\n output TEXT NOT NULL DEFAULT '',\n ran_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_results_exercise ON exercise_results(exercise_id);\n\nCREATE TABLE IF NOT EXISTS resources (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,\n title TEXT NOT NULL DEFAULT '',\n url TEXT NOT NULL DEFAULT '',\n source TEXT NOT NULL DEFAULT 'manual'\n CHECK (source IN ('manual','auto','import')),\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\nCREATE INDEX IF NOT EXISTS idx_resources_topic ON resources(topic_id);\n\nCREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL DEFAULT ''\n);\n\n-- FTS5 virtual table for full-text search over entries\nCREATE VIRTUAL TABLE IF NOT EXISTS entries_fts\n USING fts5(content, content='entries', content_rowid='id');\n\n-- Sync triggers: keep entries_fts up to date with entries\nCREATE TRIGGER IF NOT EXISTS entries_ai\n AFTER INSERT ON entries BEGIN\n INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\nCREATE TRIGGER IF NOT EXISTS entries_ad\n AFTER DELETE ON entries BEGIN\n INSERT INTO entries_fts(entries_fts, rowid, content)\n VALUES ('delete', old.id, old.content);\n END;\n\nCREATE TRIGGER IF NOT EXISTS entries_au\n AFTER UPDATE ON entries BEGIN\n INSERT INTO entries_fts(entries_fts, rowid, content)\n VALUES ('delete', old.id, old.content);\n INSERT INTO entries_fts(rowid, content) VALUES (new.id, new.content);\n END;\n";
2
2
  /** Future migrations appended here in order (v1 is baseline — empty). */
3
3
  export declare const migrations: string[];
@@ -93,6 +93,17 @@ CREATE TABLE IF NOT EXISTS exercise_results (
93
93
 
94
94
  CREATE INDEX IF NOT EXISTS idx_results_exercise ON exercise_results(exercise_id);
95
95
 
96
+ CREATE TABLE IF NOT EXISTS resources (
97
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
98
+ topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
99
+ title TEXT NOT NULL DEFAULT '',
100
+ url TEXT NOT NULL DEFAULT '',
101
+ source TEXT NOT NULL DEFAULT 'manual'
102
+ CHECK (source IN ('manual','auto','import')),
103
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
104
+ );
105
+ CREATE INDEX IF NOT EXISTS idx_resources_topic ON resources(topic_id);
106
+
96
107
  CREATE TABLE IF NOT EXISTS settings (
97
108
  key TEXT PRIMARY KEY,
98
109
  value TEXT NOT NULL DEFAULT ''
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/storage/schema.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0HrB,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,UAAU,GAAa,EAAE,CAAC"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/storage/schema.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqIrB,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,UAAU,GAAa,EAAE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ResourceService } from '../services/resources.js';
3
+ import type { SessionState } from '../types.js';
4
+ export declare function registerResourceTools(server: McpServer, svc: ResourceService, sessions: Map<string, SessionState>, notify: () => void): void;
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+ function getSession(sessions, sessionId) {
3
+ const key = sessionId || '_default';
4
+ if (!sessions.has(key)) {
5
+ sessions.set(key, { subjectId: null, topicId: null });
6
+ }
7
+ return sessions.get(key);
8
+ }
9
+ function err(text) {
10
+ return { content: [{ type: 'text', text }], isError: true };
11
+ }
12
+ function ok(text) {
13
+ return { content: [{ type: 'text', text }] };
14
+ }
15
+ export function registerResourceTools(server, svc, sessions, notify) {
16
+ // 1. learn_add_resource
17
+ server.tool('learn_add_resource', 'Add a reference link to the active topic (or a specific topic by ID)', {
18
+ title: z.string().describe('Resource title'),
19
+ url: z.string().describe('Resource URL'),
20
+ topic_id: z.number().optional().describe('Topic ID (defaults to active topic)'),
21
+ session_id: z.string().optional(),
22
+ }, async ({ title, url, topic_id, session_id }) => {
23
+ const tid = topic_id ?? getSession(sessions, session_id).topicId;
24
+ if (tid === null) {
25
+ return err('No active topic. Use learn_set_topic first or provide topic_id.');
26
+ }
27
+ const resource = svc.addResource(tid, title, url, 'manual');
28
+ notify();
29
+ return ok(`Added resource "${resource.title}" (id=${resource.id}) to topic ${tid}`);
30
+ });
31
+ // 2. learn_import_resources
32
+ server.tool('learn_import_resources', 'Bulk import resource links from a JSON array of {topic_id, title, url} objects', {
33
+ resources_json: z.string().describe('JSON array of {topic_id: number, title: string, url: string}'),
34
+ }, async ({ resources_json }) => {
35
+ let resources;
36
+ try {
37
+ resources = JSON.parse(resources_json);
38
+ }
39
+ catch {
40
+ return err('Invalid JSON');
41
+ }
42
+ if (!Array.isArray(resources)) {
43
+ return err('Expected a JSON array');
44
+ }
45
+ const count = svc.importResources(resources);
46
+ notify();
47
+ return ok(`Imported ${count} resources`);
48
+ });
49
+ }
50
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/tools/resources.ts"],"names":[],"mappings":"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"}
@@ -38,6 +38,14 @@ export interface Visualization {
38
38
  steps_json: string;
39
39
  created_at: string;
40
40
  }
41
+ export interface Resource {
42
+ id: number;
43
+ topic_id: number;
44
+ title: string;
45
+ url: string;
46
+ source: 'manual' | 'auto' | 'import';
47
+ created_at: string;
48
+ }
41
49
  export interface VizStep {
42
50
  html: string;
43
51
  description: string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "study-dash-server",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {