@bis-code/study-dash 0.2.2 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/server/dist/bundle.mjs +160 -40
- package/server/dist/dashboard/api.d.ts +3 -1
- package/server/dist/dashboard/api.js +14 -2
- package/server/dist/dashboard/api.js.map +1 -1
- package/server/dist/dashboard/server.d.ts +3 -1
- package/server/dist/dashboard/server.js +10 -3
- package/server/dist/dashboard/server.js.map +1 -1
- package/server/dist/index.js +5 -1
- package/server/dist/index.js.map +1 -1
- package/server/dist/services/resources.d.ts +16 -0
- package/server/dist/services/resources.js +27 -0
- package/server/dist/services/resources.js.map +1 -0
- package/server/dist/storage/schema.d.ts +1 -1
- package/server/dist/storage/schema.js +11 -0
- package/server/dist/storage/schema.js.map +1 -1
- package/server/dist/tools/resources.d.ts +4 -0
- package/server/dist/tools/resources.js +50 -0
- package/server/dist/tools/resources.js.map +1 -0
- package/server/dist/types.d.ts +8 -0
- package/server/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bis-code/study-dash",
|
|
3
|
-
"version": "0.
|
|
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>",
|
package/server/dist/bundle.mjs
CHANGED
|
@@ -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 (
|
|
454
|
-
const execErr =
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
1022
|
-
const msg =
|
|
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 (
|
|
1043
|
-
const msg =
|
|
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 (
|
|
1060
|
-
const msg =
|
|
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
|
|
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(
|
|
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
|
|
1982
|
-
|
|
1983
|
-
|
|
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
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
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((
|
|
2256
|
-
console.error("Fatal:",
|
|
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
|
-
|
|
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":"
|
|
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":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAM7B,OAAO,EACL,cAAc,EACd,YAAY,EACZ,WAAW,EACX,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,SAAS,GACV,MAAM,UAAU,CAAC;AAElB,gCAAgC;AAChC,uEAAuE;AACvE,mCAAmC;AACnC,OAAO,SAAS,MAAM,qBAAqB,CAAC;AAC5C,mCAAmC;AACnC,OAAO,KAAK,MAAM,iBAAiB,CAAC;AACpC,mCAAmC;AACnC,OAAO,SAAS,MAAM,qBAAqB,CAAC;AAE5C,MAAM,YAAY,GAA6D;IAC7E,GAAG,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACpE,aAAa,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,0BAA0B,EAAE;IAC9E,SAAS,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACnF,aAAa,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,yBAAyB,EAAE;CAC9E,CAAC;AAEF,MAAM,OAAO,eAAe;IAKhB;IACA;IACA;IACA;IACA;IACA;IATF,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC5C,UAAU,GAAuB,IAAI,CAAC;IAE9C,YACU,aAAgC,EAChC,KAAgB,EAChB,MAAkB,EAClB,WAA4B,EAC5B,WAA4B,EAC5B,IAAY;QALZ,kBAAa,GAAb,aAAa,CAAmB;QAChC,UAAK,GAAL,KAAK,CAAW;QAChB,WAAM,GAAN,MAAM,CAAY;QAClB,gBAAW,GAAX,WAAW,CAAiB;QAC5B,gBAAW,GAAX,WAAW,CAAiB;QAC5B,SAAI,GAAJ,IAAI,CAAQ;IACnB,CAAC;IAEJ,KAAK;QACH,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QAChF,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE;YAClD,OAAO,CAAC,KAAK,CAAC,yCAAyC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;YACxB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;IACH,CAAC;IAED,MAAM;QACJ,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC9E,MAAM,OAAO,GAAG,SAAS,IAAI,MAAM,CAAC;QACpC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACrC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,0EAA0E;IAElE,aAAa,CAAC,GAAyB,EAAE,GAAwB;QACvE,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;QAC3B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,KAAK,CAAC;QAEnC,+BAA+B;QAC/B,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACtB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;YACxC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAC/F,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;gBAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAC,CAAC;gBAChE,OAAO;YACT,CAAC;QACH,CAAC;QAED,eAAe;QACf,IAAI,GAAG,KAAK,aAAa,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YAC9C,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QAED,aAAa;QACb,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YACrC,OAAO;QACT,CAAC;QAED,eAAe;QACf,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,0EAA0E;IAElE,SAAS,CAAC,IAA0B,EAAE,GAAwB;QACpE,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;YACjB,cAAc,EAAE,mBAAmB;YACnC,eAAe,EAAE,UAAU;YAC3B,YAAY,EAAE,YAAY;YAC1B,6BAA6B,EAAE,GAAG;SACnC,CAAC,CAAC;QAEH,+BAA+B;QAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QACtF,GAAG,CAAC,KAAK,CAAC,SAAS,SAAS,MAAM,CAAC,CAAC;QAEpC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACzB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACnB,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,0EAA0E;IAElE,QAAQ,CAAC,MAAc,EAAE,GAAW,EAAE,GAAyB,EAAE,GAAwB;QAC/F,0CAA0C;QAC1C,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAE/B,oBAAoB;QACpB,IAAI,MAAM,KAAK,KAAK,IAAI,IAAI,KAAK,eAAe,EAAE,CAAC;YACjD,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC7C,OAAO;QACT,CAAC;QAED,+BAA+B;QAC/B,IAAI,MAAM,KAAK,KAAK,IAAI,gCAAgC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACpE,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC3C,OAAO;QACT,CAAC;QAED,0BAA0B;QAC1B,IAAI,MAAM,KAAK,KAAK,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/D,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACtC,OAAO;QACT,CAAC;QAED,gCAAgC;QAChC,IAAI,MAAM,KAAK,KAAK,IAAI,iCAAiC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACrE,oBAAoB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QAED,gCAAgC;QAChC,IAAI,MAAM,KAAK,KAAK,IAAI,iCAAiC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACrE,oBAAoB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QAED,sBAAsB;QACtB,IAAI,MAAM,KAAK,KAAK,IAAI,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1D,WAAW,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACxE,OAAO;QACT,CAAC;QAED,8BAA8B;QAC9B,IAAI,MAAM,KAAK,MAAM,IAAI,8BAA8B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACnE,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC3C,OAAO;QACT,CAAC;QAED,iCAAiC;QACjC,IAAI,MAAM,KAAK,MAAM,IAAI,iCAAiC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACtE,gBAAgB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC7C,OAAO;QACT,CAAC;QAED,wBAAwB;QACxB,IAAI,MAAM,KAAK,KAAK,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;YAC/C,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACnC,OAAO;QACT,CAAC;QAED,6BAA6B;QAC7B,SAAS,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,GAAG,CAAC,CAAC;IAC9C,CAAC;IAED,0EAA0E;IAElE,WAAW,CAAC,GAAW,EAAE,GAAwB;QACvD,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,IAAI,EAAE,CAAC;YACT,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YACzD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtB,OAAO;QACT,CAAC;QAED,6CAA6C;QAC7C,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,EAAE,CAAC;YACV,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;YAC1D,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACvB,OAAO;QACT,CAAC;QAED,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;QACrD,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvB,CAAC;CACF"}
|
package/server/dist/index.js
CHANGED
|
@@ -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();
|
package/server/dist/index.js.map
CHANGED
|
@@ -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;
|
|
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
|
|
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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"}
|
package/server/dist/types.d.ts
CHANGED
|
@@ -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;
|