@bis-code/study-dash 0.4.1 → 0.5.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 +131 -6
- package/server/dist/dashboard/api.d.ts +2 -0
- package/server/dist/dashboard/api.js +37 -0
- package/server/dist/dashboard/api.js.map +1 -1
- package/server/dist/dashboard/server.js +11 -1
- package/server/dist/dashboard/server.js.map +1 -1
- package/server/dist/index.js +5 -0
- package/server/dist/index.js.map +1 -1
- package/server/dist/services/exercises.d.ts +10 -0
- package/server/dist/services/exercises.js +77 -2
- package/server/dist/services/exercises.js.map +1 -1
- 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.5.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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process$1 from 'node:process';
|
|
3
3
|
import BetterSqlite3 from 'better-sqlite3';
|
|
4
|
-
import fs, { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import fs, { mkdirSync, writeFileSync, existsSync, readFileSync, renameSync } from 'node:fs';
|
|
5
5
|
import path, { join } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { execFile } from 'node:child_process';
|
|
@@ -22952,7 +22952,7 @@ class ExerciseService {
|
|
|
22952
22952
|
const subject = this.getSubjectForTopic(topicId);
|
|
22953
22953
|
if (subject) {
|
|
22954
22954
|
const exerciseSlug = slugify(title);
|
|
22955
|
-
const ext = extensionForLanguage(subject.language);
|
|
22955
|
+
const ext = extensionForLanguage(subject.language.toLowerCase());
|
|
22956
22956
|
const files = {};
|
|
22957
22957
|
if (starter_code) {
|
|
22958
22958
|
files[`main${ext}`] = starter_code;
|
|
@@ -22989,7 +22989,7 @@ class ExerciseService {
|
|
|
22989
22989
|
javascript: { command: 'npx', args: ['vitest', 'run'] },
|
|
22990
22990
|
typescript: { command: 'npx', args: ['vitest', 'run'] },
|
|
22991
22991
|
};
|
|
22992
|
-
const config = commandMap[subject.language];
|
|
22992
|
+
const config = commandMap[subject.language.toLowerCase()];
|
|
22993
22993
|
if (!config)
|
|
22994
22994
|
throw new Error(`Unsupported language: ${subject.language}`);
|
|
22995
22995
|
let stdout = '';
|
|
@@ -23112,6 +23112,79 @@ class ExerciseService {
|
|
|
23112
23112
|
.prepare('SELECT * FROM exercises WHERE topic_id = ? ORDER BY created_at ASC, id ASC')
|
|
23113
23113
|
.all(topicId);
|
|
23114
23114
|
}
|
|
23115
|
+
getExerciseFiles(exerciseId) {
|
|
23116
|
+
const exercise = this.db.raw
|
|
23117
|
+
.prepare('SELECT * FROM exercises WHERE id = ?')
|
|
23118
|
+
.get(exerciseId);
|
|
23119
|
+
if (!exercise)
|
|
23120
|
+
return undefined;
|
|
23121
|
+
const subject = this.getSubjectForTopic(exercise.topic_id);
|
|
23122
|
+
const lang = subject?.language.toLowerCase() ?? '';
|
|
23123
|
+
const ext = extensionForLanguage(lang);
|
|
23124
|
+
const mainFile = `main${ext}`;
|
|
23125
|
+
const testFile = `main_test${ext}`;
|
|
23126
|
+
let main = '';
|
|
23127
|
+
let test = '';
|
|
23128
|
+
if (exercise.file_path) {
|
|
23129
|
+
try {
|
|
23130
|
+
main = readFileSync(join(exercise.file_path, mainFile), 'utf-8');
|
|
23131
|
+
}
|
|
23132
|
+
catch { }
|
|
23133
|
+
try {
|
|
23134
|
+
test = readFileSync(join(exercise.file_path, testFile), 'utf-8');
|
|
23135
|
+
}
|
|
23136
|
+
catch { }
|
|
23137
|
+
}
|
|
23138
|
+
return { main, test, language: lang, mainFile, testFile };
|
|
23139
|
+
}
|
|
23140
|
+
saveExerciseFiles(exerciseId, main, test) {
|
|
23141
|
+
const exercise = this.db.raw
|
|
23142
|
+
.prepare('SELECT * FROM exercises WHERE id = ?')
|
|
23143
|
+
.get(exerciseId);
|
|
23144
|
+
if (!exercise)
|
|
23145
|
+
throw new Error(`Exercise ${exerciseId} not found`);
|
|
23146
|
+
const subject = this.getSubjectForTopic(exercise.topic_id);
|
|
23147
|
+
if (!subject)
|
|
23148
|
+
throw new Error(`No subject found for exercise ${exerciseId}`);
|
|
23149
|
+
const lang = subject.language.toLowerCase();
|
|
23150
|
+
const ext = extensionForLanguage(lang);
|
|
23151
|
+
let filePath = exercise.file_path;
|
|
23152
|
+
if (!filePath) {
|
|
23153
|
+
const exerciseSlug = slugify(exercise.title);
|
|
23154
|
+
filePath = this.fileStore.writeExerciseFiles(subject.slug, exerciseSlug, {});
|
|
23155
|
+
this.db.raw.prepare('UPDATE exercises SET file_path = ? WHERE id = ?').run(filePath, exerciseId);
|
|
23156
|
+
}
|
|
23157
|
+
writeFileSync(join(filePath, `main${ext}`), main, 'utf-8');
|
|
23158
|
+
writeFileSync(join(filePath, `main_test${ext}`), test, 'utf-8');
|
|
23159
|
+
}
|
|
23160
|
+
migrateFileExtensions() {
|
|
23161
|
+
const exercises = this.db.raw
|
|
23162
|
+
.prepare("SELECT * FROM exercises WHERE file_path != '' AND type IN ('coding', 'project')")
|
|
23163
|
+
.all();
|
|
23164
|
+
let migrated = 0;
|
|
23165
|
+
for (const exercise of exercises) {
|
|
23166
|
+
const subject = this.getSubjectForTopic(exercise.topic_id);
|
|
23167
|
+
if (!subject)
|
|
23168
|
+
continue;
|
|
23169
|
+
const ext = extensionForLanguage(subject.language.toLowerCase());
|
|
23170
|
+
if (ext === '.txt')
|
|
23171
|
+
continue;
|
|
23172
|
+
const dir = exercise.file_path;
|
|
23173
|
+
const mainTxt = join(dir, 'main.txt');
|
|
23174
|
+
const testTxt = join(dir, 'main_test.txt');
|
|
23175
|
+
const mainTarget = join(dir, `main${ext}`);
|
|
23176
|
+
const testTarget = join(dir, `main_test${ext}`);
|
|
23177
|
+
if (existsSync(mainTxt) && !existsSync(mainTarget)) {
|
|
23178
|
+
renameSync(mainTxt, mainTarget);
|
|
23179
|
+
migrated++;
|
|
23180
|
+
}
|
|
23181
|
+
if (existsSync(testTxt) && !existsSync(testTarget)) {
|
|
23182
|
+
renameSync(testTxt, testTarget);
|
|
23183
|
+
migrated++;
|
|
23184
|
+
}
|
|
23185
|
+
}
|
|
23186
|
+
return migrated;
|
|
23187
|
+
}
|
|
23115
23188
|
getSubjectForTopic(topicId) {
|
|
23116
23189
|
return this.db.raw
|
|
23117
23190
|
.prepare(`SELECT s.* FROM subjects s
|
|
@@ -23679,6 +23752,43 @@ function handleSearch(qaSvc) {
|
|
|
23679
23752
|
}
|
|
23680
23753
|
};
|
|
23681
23754
|
}
|
|
23755
|
+
function handleExerciseFiles(exerciseSvc) {
|
|
23756
|
+
return (req, res) => {
|
|
23757
|
+
const id = extractId(req.url ?? '', '/api/exercises/');
|
|
23758
|
+
if (id === null) {
|
|
23759
|
+
writeError(res, 400, 'Invalid exercise ID');
|
|
23760
|
+
return;
|
|
23761
|
+
}
|
|
23762
|
+
const files = exerciseSvc.getExerciseFiles(id);
|
|
23763
|
+
if (!files) {
|
|
23764
|
+
writeError(res, 404, 'Exercise not found');
|
|
23765
|
+
return;
|
|
23766
|
+
}
|
|
23767
|
+
writeJSON(res, files);
|
|
23768
|
+
};
|
|
23769
|
+
}
|
|
23770
|
+
function handleSaveExerciseFiles(exerciseSvc) {
|
|
23771
|
+
return async (req, res) => {
|
|
23772
|
+
const id = extractId(req.url ?? '', '/api/exercises/');
|
|
23773
|
+
if (id === null) {
|
|
23774
|
+
writeError(res, 400, 'Invalid exercise ID');
|
|
23775
|
+
return;
|
|
23776
|
+
}
|
|
23777
|
+
try {
|
|
23778
|
+
const body = (await parseBody(req));
|
|
23779
|
+
if (typeof body?.main !== 'string' || typeof body?.test !== 'string') {
|
|
23780
|
+
writeError(res, 400, 'Request body must have "main" and "test" strings');
|
|
23781
|
+
return;
|
|
23782
|
+
}
|
|
23783
|
+
exerciseSvc.saveExerciseFiles(id, body.main, body.test);
|
|
23784
|
+
writeJSON(res, { ok: true });
|
|
23785
|
+
}
|
|
23786
|
+
catch (err) {
|
|
23787
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23788
|
+
writeError(res, 500, msg);
|
|
23789
|
+
}
|
|
23790
|
+
};
|
|
23791
|
+
}
|
|
23682
23792
|
function handleResourceFile(resourceSvc) {
|
|
23683
23793
|
return (req, res) => {
|
|
23684
23794
|
const id = extractId(req.url ?? '', '/api/resources/');
|
|
@@ -23724,11 +23834,11 @@ function handleResourceFile(resourceSvc) {
|
|
|
23724
23834
|
};
|
|
23725
23835
|
}
|
|
23726
23836
|
|
|
23727
|
-
var indexHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>StudyDash</title>\n <link rel=\"stylesheet\" href=\"styles.css\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css\">\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/sql.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/typescript.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js\"></script>\n</head>\n<body>\n\n <!-- Desktop sidebar (hidden on mobile) -->\n <div id=\"desktop-sidebar\">\n <div class=\"sidebar-inner\">\n <h1 style=\"font-size:18px;font-weight:700;color:var(--accent);margin-bottom:12px;\">StudyDash</h1>\n <div id=\"sidebar-subjects\" class=\"subject-switcher\"></div>\n <div class=\"progress-bar\" style=\"margin-bottom:14px;\">\n <div id=\"sidebar-progress-fill\" class=\"progress-fill\" style=\"width:0%\"></div>\n <span id=\"sidebar-progress-text\" class=\"progress-text\">0 / 0 topics</span>\n </div>\n <div id=\"sidebar-phases\"></div>\n </div>\n <div class=\"sidebar-footer\">\n <span class=\"sse-dot disconnected\" id=\"sse-dot\"></span>\n <kbd>Ctrl+K</kbd> Search\n </div>\n </div>\n\n <div class=\"page-container\">\n\n <!-- ==================== PAGE: HOME ==================== -->\n <div id=\"page-home\" class=\"page active\">\n <div class=\"page-header\">\n <h1>StudyDash</h1>\n </div>\n <div id=\"home-subjects\" class=\"subject-switcher\"></div>\n <div class=\"progress-bar\">\n <div id=\"home-progress-fill\" class=\"progress-fill\" style=\"width:0%\"></div>\n <span id=\"home-progress-text\" class=\"progress-text\">0 / 0 topics</span>\n </div>\n <div id=\"home-stats\" class=\"stats-grid\"></div>\n <div class=\"section-divider\">Recently Active</div>\n <div id=\"home-recent\"></div>\n </div>\n\n <!-- ==================== PAGE: TOPICS ==================== -->\n <div id=\"page-topics\" class=\"page\">\n <div class=\"page-header\">\n <h1>Topics</h1>\n </div>\n <div id=\"topics-subjects\" class=\"subject-switcher\"></div>\n <div id=\"topics-phases\"></div>\n </div>\n\n <!-- ==================== PAGE: TOPIC DETAIL ==================== -->\n <div id=\"page-topic\" class=\"page\">\n <button class=\"back-btn\" onclick=\"showPage('topics')\">← Back to Topics</button>\n <div class=\"topic-title-row\">\n <h2 id=\"topic-name\"></h2>\n <span id=\"topic-status\" class=\"badge\"></span>\n </div>\n <p id=\"topic-desc\" class=\"topic-desc\"></p>\n <div class=\"tabs\">\n <button class=\"tab-btn active\" data-tab=\"qa\" onclick=\"switchTab('qa')\">Q&A</button>\n <button class=\"tab-btn\" data-tab=\"viz\" onclick=\"switchTab('viz')\">Visualize</button>\n <button class=\"tab-btn\" data-tab=\"exercises\" onclick=\"switchTab('exercises')\">Exercises</button>\n <button class=\"tab-btn\" data-tab=\"resources\" onclick=\"switchTab('resources')\">Resources</button>\n </div>\n <div id=\"tab-qa\" class=\"tab-panel active\"></div>\n <div id=\"tab-viz\" class=\"tab-panel\"></div>\n <div id=\"tab-exercises\" class=\"tab-panel\"></div>\n <div id=\"tab-resources\" class=\"tab-panel\"></div>\n </div>\n\n <!-- ==================== PAGE: SEARCH ==================== -->\n <div id=\"page-search\" class=\"page\">\n <div class=\"page-header\">\n <h1>Search</h1>\n </div>\n <div class=\"search-bar\">\n <input type=\"text\" id=\"search-input\" placeholder=\"Search questions, answers, notes...\" autocomplete=\"off\">\n </div>\n <div id=\"search-results\"></div>\n </div>\n\n </div>\n\n <!-- Mobile bottom nav -->\n <nav class=\"mobile-nav\">\n <button class=\"nav-btn active\" data-page=\"home\" onclick=\"showPage('home')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/><polyline points=\"9 22 9 12 15 12 15 22\"/></svg>\n Home\n </button>\n <button class=\"nav-btn\" data-page=\"topics\" onclick=\"showPage('topics')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"/><path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z\"/></svg>\n Topics\n </button>\n <button class=\"nav-btn\" data-page=\"search\" onclick=\"showPage('search')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/></svg>\n Search\n </button>\n </nav>\n\n <!-- Search modal (desktop Ctrl+K) -->\n <div id=\"search-modal\" class=\"modal hidden\">\n <div class=\"modal-backdrop\" onclick=\"closeSearchModal()\"></div>\n <div class=\"modal-content\">\n <input id=\"modal-search-input\" type=\"text\" placeholder=\"Search questions, answers, notes...\" autocomplete=\"off\">\n <div id=\"modal-search-results\" class=\"modal-results\"></div>\n </div>\n </div>\n\n <script src=\"app.js\"></script>\n</body>\n</html>\n";
|
|
23837
|
+
var indexHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>StudyDash</title>\n <link rel=\"stylesheet\" href=\"styles.css\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css\">\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/sql.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/typescript.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/rust.min.js\"></script>\n</head>\n<body>\n\n <!-- Desktop sidebar (hidden on mobile) -->\n <div id=\"desktop-sidebar\">\n <div class=\"sidebar-inner\">\n <h1 style=\"font-size:18px;font-weight:700;color:var(--accent);margin-bottom:12px;\">StudyDash</h1>\n <div id=\"sidebar-subjects\" class=\"subject-switcher\"></div>\n <div class=\"progress-bar\" style=\"margin-bottom:14px;\">\n <div id=\"sidebar-progress-fill\" class=\"progress-fill\" style=\"width:0%\"></div>\n <span id=\"sidebar-progress-text\" class=\"progress-text\">0 / 0 topics</span>\n </div>\n <div id=\"sidebar-phases\"></div>\n </div>\n <div class=\"sidebar-footer\">\n <span class=\"sse-dot disconnected\" id=\"sse-dot\"></span>\n <kbd>Ctrl+K</kbd> Search\n </div>\n </div>\n\n <div class=\"page-container\">\n\n <!-- ==================== PAGE: HOME ==================== -->\n <div id=\"page-home\" class=\"page active\">\n <div class=\"page-header\">\n <h1>StudyDash</h1>\n </div>\n <div id=\"home-subjects\" class=\"subject-switcher\"></div>\n <div class=\"progress-bar\">\n <div id=\"home-progress-fill\" class=\"progress-fill\" style=\"width:0%\"></div>\n <span id=\"home-progress-text\" class=\"progress-text\">0 / 0 topics</span>\n </div>\n <div id=\"home-stats\" class=\"stats-grid\"></div>\n <div class=\"section-divider\">Recently Active</div>\n <div id=\"home-recent\"></div>\n </div>\n\n <!-- ==================== PAGE: TOPICS ==================== -->\n <div id=\"page-topics\" class=\"page\">\n <div class=\"page-header\">\n <h1>Topics</h1>\n </div>\n <div id=\"topics-subjects\" class=\"subject-switcher\"></div>\n <div id=\"topics-phases\"></div>\n </div>\n\n <!-- ==================== PAGE: TOPIC DETAIL ==================== -->\n <div id=\"page-topic\" class=\"page\">\n <button class=\"back-btn\" onclick=\"showPage('topics')\">← Back to Topics</button>\n <div class=\"topic-title-row\">\n <h2 id=\"topic-name\"></h2>\n <span id=\"topic-status\" class=\"badge\"></span>\n </div>\n <p id=\"topic-desc\" class=\"topic-desc\"></p>\n <div class=\"tabs\">\n <button class=\"tab-btn active\" data-tab=\"qa\" onclick=\"switchTab('qa')\">Q&A</button>\n <button class=\"tab-btn\" data-tab=\"viz\" onclick=\"switchTab('viz')\">Visualize</button>\n <button class=\"tab-btn\" data-tab=\"exercises\" onclick=\"switchTab('exercises')\">Exercises</button>\n <button class=\"tab-btn\" data-tab=\"resources\" onclick=\"switchTab('resources')\">Resources</button>\n </div>\n <div id=\"tab-qa\" class=\"tab-panel active\"></div>\n <div id=\"tab-viz\" class=\"tab-panel\"></div>\n <div id=\"tab-exercises\" class=\"tab-panel\"></div>\n <div id=\"tab-resources\" class=\"tab-panel\"></div>\n </div>\n\n <!-- ==================== PAGE: EXERCISE EDITOR ==================== -->\n <div id=\"page-exercise-editor\" class=\"page\">\n <div class=\"exercise-editor\">\n <div class=\"exercise-editor-problem\" id=\"editor-problem\"></div>\n <div class=\"exercise-editor-right\">\n <div class=\"exercise-editor-code\">\n <div class=\"editor-tabs\">\n <button class=\"editor-tab active\" id=\"tab-main\" onclick=\"switchEditorTab('main')\">main.go</button>\n <button class=\"editor-tab\" id=\"tab-test\" onclick=\"switchEditorTab('test')\">main_test.go</button>\n <button class=\"editor-back-btn\" onclick=\"closeExerciseEditor()\">← Back</button>\n </div>\n <div id=\"editor-container\"></div>\n </div>\n <div class=\"exercise-editor-output\" id=\"editor-output\">\n <div class=\"editor-output-header\">\n <span>Test Output</span>\n <button class=\"editor-run-btn\" id=\"editor-run-btn\" onclick=\"runTestsFromEditor()\">Run Tests</button>\n </div>\n <div class=\"editor-output-body\" id=\"editor-output-body\">\n <span class=\"text-muted\">Click \"Run Tests\" or press Ctrl+Enter</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- ==================== PAGE: SEARCH ==================== -->\n <div id=\"page-search\" class=\"page\">\n <div class=\"page-header\">\n <h1>Search</h1>\n </div>\n <div class=\"search-bar\">\n <input type=\"text\" id=\"search-input\" placeholder=\"Search questions, answers, notes...\" autocomplete=\"off\">\n </div>\n <div id=\"search-results\"></div>\n </div>\n\n </div>\n\n <!-- Mobile bottom nav -->\n <nav class=\"mobile-nav\">\n <button class=\"nav-btn active\" data-page=\"home\" onclick=\"showPage('home')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/><polyline points=\"9 22 9 12 15 12 15 22\"/></svg>\n Home\n </button>\n <button class=\"nav-btn\" data-page=\"topics\" onclick=\"showPage('topics')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"/><path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z\"/></svg>\n Topics\n </button>\n <button class=\"nav-btn\" data-page=\"search\" onclick=\"showPage('search')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/></svg>\n Search\n </button>\n </nav>\n\n <!-- Search modal (desktop Ctrl+K) -->\n <div id=\"search-modal\" class=\"modal hidden\">\n <div class=\"modal-backdrop\" onclick=\"closeSearchModal()\"></div>\n <div class=\"modal-content\">\n <input id=\"modal-search-input\" type=\"text\" placeholder=\"Search questions, answers, notes...\" autocomplete=\"off\">\n <div id=\"modal-search-results\" class=\"modal-results\"></div>\n </div>\n </div>\n\n <script src=\"app.js\"></script>\n</body>\n</html>\n";
|
|
23728
23838
|
|
|
23729
|
-
var appJs = "// StudyDash — Dashboard Application\n// Connects to the learn-cc API and renders a responsive learning dashboard.\n\n// --- Markdown config ---\nmarked.setOptions({\n highlight: function(code, lang) {\n if (lang && hljs.getLanguage(lang)) {\n return hljs.highlight(code, { language: lang }).value;\n }\n return hljs.highlightAuto(code).value;\n },\n breaks: true,\n gfm: true,\n});\n\n// --- State ---\nconst state = {\n subjects: [],\n activeSubject: null,\n phases: [],\n activeTopic: null,\n activeTab: 'qa',\n topicData: null,\n topicViz: [],\n topicExercises: [],\n topicResources: [],\n searchTimeout: null,\n vizIndex: 0,\n vizStep: 0,\n};\n\n// --- API Helper ---\nasync function api(path) {\n const res = await fetch(path);\n if (!res.ok) throw new Error(`API error: ${res.status}`);\n return res.json();\n}\n\n// --- Sanitize viz HTML ---\nfunction sanitizeVizHtml(html) {\n const allowed = ['div', 'span', 'small', 'br', 'code', 'strong', 'em'];\n const tmp = document.createElement('div');\n tmp.innerHTML = html;\n // Remove all script tags\n tmp.querySelectorAll('script').forEach(el => el.remove());\n // Walk all elements, remove disallowed tags, strip non-class/style attributes\n const walk = (node) => {\n const children = [...node.children];\n for (const child of children) {\n if (!allowed.includes(child.tagName.toLowerCase())) {\n child.replaceWith(...child.childNodes);\n } else {\n [...child.attributes].forEach(attr => {\n if (attr.name !== 'class' && attr.name !== 'style') child.removeAttribute(attr.name);\n });\n walk(child);\n }\n }\n };\n walk(tmp);\n return tmp.innerHTML;\n}\n\n// --- Escape HTML for text content in templates ---\nfunction escapeHtml(str) {\n if (!str) return '';\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n}\n\n// --- Format time ---\nfunction formatTime(iso) {\n if (!iso) return '';\n const d = new Date(iso);\n return d.toLocaleDateString('en-US', {\n month: 'short', day: 'numeric', year: 'numeric',\n hour: '2-digit', minute: '2-digit',\n });\n}\n\nfunction truncate(text, max) {\n if (!text) return '';\n if (text.length <= max) return escapeHtml(text);\n return escapeHtml(text.substring(0, max)) + '...';\n}\n\n// --- On load ---\ndocument.addEventListener('DOMContentLoaded', async () => {\n try {\n state.subjects = await api('/api/subjects');\n } catch {\n state.subjects = [];\n }\n\n if (state.subjects.length > 0) {\n state.activeSubject = state.subjects[0];\n await loadSubject();\n }\n\n renderAllSubjectSwitchers();\n renderHome();\n connectSSE();\n});\n\n// --- SSE ---\nfunction connectSSE() {\n const dot = document.getElementById('sse-dot');\n let evtSource;\n\n function connect() {\n evtSource = new EventSource('/api/events');\n\n evtSource.onopen = () => {\n if (dot) { dot.classList.remove('disconnected'); dot.classList.add('connected'); }\n };\n\n evtSource.onmessage = async (event) => {\n try {\n const data = JSON.parse(event.data);\n if (data.type === 'update') {\n await refresh();\n }\n } catch { /* ignore parse errors */ }\n };\n\n evtSource.onerror = () => {\n if (dot) { dot.classList.remove('connected'); dot.classList.add('disconnected'); }\n evtSource.close();\n setTimeout(connect, 3000);\n };\n }\n\n connect();\n}\n\nasync function refresh() {\n // Re-fetch subjects\n try {\n state.subjects = await api('/api/subjects');\n } catch { /* keep existing */ }\n\n if (state.activeSubject) {\n // Refresh the active subject from the new list\n const updated = state.subjects.find(s => s.id === state.activeSubject.id);\n if (updated) state.activeSubject = updated;\n await loadSubject();\n }\n\n renderAllSubjectSwitchers();\n\n // Re-render current view\n const activePage = document.querySelector('.page.active');\n if (activePage) {\n const pageId = activePage.id.replace('page-', '');\n if (pageId === 'home') renderHome();\n else if (pageId === 'topics') renderTopicsPage();\n else if (pageId === 'topic' && state.activeTopic) await selectTopic(state.activeTopic);\n }\n}\n\n// --- Subject management ---\nasync function loadSubject() {\n if (!state.activeSubject) return;\n try {\n state.phases = await api(`/api/subjects/${state.activeSubject.id}/phases`);\n } catch {\n state.phases = [];\n }\n renderSidebar();\n}\n\nasync function switchSubject(id) {\n const subject = state.subjects.find(s => s.id === id);\n if (!subject) return;\n state.activeSubject = subject;\n state.activeTopic = null;\n state.topicData = null;\n await loadSubject();\n renderAllSubjectSwitchers();\n renderHome();\n renderTopicsPage();\n // If on topic detail page, go back to topics\n const activePage = document.querySelector('.page.active');\n if (activePage && activePage.id === 'page-topic') {\n showPage('topics');\n }\n}\n\nfunction renderAllSubjectSwitchers() {\n const containers = ['home-subjects', 'topics-subjects', 'sidebar-subjects'];\n containers.forEach(id => {\n const el = document.getElementById(id);\n if (!el) return;\n if (state.subjects.length === 0) {\n el.innerHTML = '';\n return;\n }\n el.innerHTML = state.subjects.map(s =>\n `<button class=\"subject-btn ${state.activeSubject && state.activeSubject.id === s.id ? 'active' : ''}\"\n onclick=\"switchSubject(${s.id})\">${escapeHtml(s.name)}</button>`\n ).join('');\n });\n}\n\n// --- Progress ---\nfunction renderProgress() {\n if (!state.activeSubject) return;\n const p = state.activeSubject.progress || {};\n const total = p.total_topics || 0;\n const done = p.done || 0;\n const pct = total > 0 ? Math.round((done / total) * 100) : 0;\n const text = `${done} / ${total} topics`;\n\n // Home progress\n const hFill = document.getElementById('home-progress-fill');\n const hText = document.getElementById('home-progress-text');\n if (hFill) hFill.style.width = pct + '%';\n if (hText) hText.textContent = text;\n\n // Sidebar progress\n const sFill = document.getElementById('sidebar-progress-fill');\n const sText = document.getElementById('sidebar-progress-text');\n if (sFill) sFill.style.width = pct + '%';\n if (sText) sText.textContent = text;\n}\n\n// --- Sidebar ---\nfunction renderSidebar() {\n renderProgress();\n const container = document.getElementById('sidebar-phases');\n if (!container) return;\n\n if (state.phases.length === 0) {\n container.innerHTML = '<div class=\"empty-state\"><p>No phases yet</p></div>';\n return;\n }\n\n container.innerHTML = state.phases.map(phase => {\n const topics = phase.topics || [];\n const doneCount = topics.filter(t => t.status === 'done').length;\n return `\n <div class=\"phase-group\">\n <div class=\"phase-header\" onclick=\"togglePhase(this)\">\n ${escapeHtml(phase.name)}\n <span style=\"font-size:10px;color:var(--text-muted)\">${doneCount}/${topics.length}</span>\n <span class=\"chevron\">▼</span>\n </div>\n <div class=\"phase-topics\">\n ${topics.map(t => `\n <div class=\"topic-item ${state.activeTopic === t.id ? 'active' : ''}\"\n onclick=\"selectTopic(${t.id})\"\n data-topic-id=\"${t.id}\">\n <span class=\"status-dot ${escapeHtml(t.status)}\"></span>\n <span>${escapeHtml(t.name)}</span>\n </div>\n `).join('')}\n </div>\n </div>`;\n }).join('');\n}\n\nfunction togglePhase(el) {\n el.classList.toggle('collapsed');\n el.nextElementSibling.classList.toggle('collapsed');\n}\n\n// --- Home page ---\nfunction renderHome() {\n renderProgress();\n\n const statsEl = document.getElementById('home-stats');\n const recentEl = document.getElementById('home-recent');\n\n if (state.subjects.length === 0) {\n if (statsEl) statsEl.innerHTML = '';\n if (recentEl) recentEl.innerHTML = `\n <div class=\"empty-state\">\n <p>Welcome to StudyDash!</p>\n <p>Start by creating a subject with <code>/learn</code></p>\n </div>`;\n return;\n }\n\n const p = state.activeSubject ? (state.activeSubject.progress || {}) : {};\n\n if (statsEl) {\n statsEl.innerHTML = `\n <div class=\"stat-card\">\n <div class=\"stat-value\">${p.done || 0}</div>\n <div class=\"stat-label\">Topics Done</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-value green\">${p.total_entries || 0}</div>\n <div class=\"stat-label\">Q&A Entries</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-value yellow\">${p.total_exercises || 0}</div>\n <div class=\"stat-label\">Exercises</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-value purple\">${p.total_viz || 0}</div>\n <div class=\"stat-label\">Visualizations</div>\n </div>`;\n }\n\n // Show recently active topics (in_progress first, then by updated_at)\n if (recentEl) {\n const allTopics = state.phases.flatMap(ph => (ph.topics || []).map(t => ({ ...t, phaseName: ph.name })));\n const active = allTopics\n .filter(t => t.status !== 'todo')\n .sort((a, b) => {\n if (a.status === 'in_progress' && b.status !== 'in_progress') return -1;\n if (b.status === 'in_progress' && a.status !== 'in_progress') return 1;\n return new Date(b.updated_at || 0) - new Date(a.updated_at || 0);\n })\n .slice(0, 5);\n\n if (active.length === 0) {\n recentEl.innerHTML = `<div class=\"empty-state\"><p>No active topics yet. Import a curriculum with <code>/learn import</code></p></div>`;\n } else {\n recentEl.innerHTML = active.map(t => `\n <div class=\"exercise-card\" style=\"cursor:pointer\" onclick=\"selectTopic(${t.id})\">\n <div class=\"exercise-header\">\n <span class=\"exercise-title\">${escapeHtml(t.name)}</span>\n <span class=\"badge ${escapeHtml(t.status)}\">${escapeHtml(t.status.replace('_', ' '))}</span>\n </div>\n <div class=\"exercise-desc\">${escapeHtml(t.phaseName)}</div>\n </div>\n `).join('');\n }\n }\n}\n\n// --- Topics page ---\nfunction renderTopicsPage() {\n const container = document.getElementById('topics-phases');\n if (!container) return;\n\n if (state.phases.length === 0) {\n container.innerHTML = `<div class=\"empty-state\"><p>No topics yet.</p><p>Import a curriculum with <code>/learn import</code></p></div>`;\n return;\n }\n\n container.innerHTML = state.phases.map(phase => {\n const topics = phase.topics || [];\n return `\n <div class=\"phase-group\">\n <div class=\"phase-header\" onclick=\"togglePhase(this)\">\n ${escapeHtml(phase.name)}\n <span class=\"chevron\">▼</span>\n </div>\n <div class=\"phase-topics\">\n ${topics.map(t => `\n <div class=\"topic-item ${state.activeTopic === t.id ? 'active' : ''}\"\n onclick=\"selectTopic(${t.id})\">\n <span class=\"status-dot ${escapeHtml(t.status)}\"></span>\n ${escapeHtml(t.name)}\n <span class=\"topic-count\"></span>\n </div>\n `).join('')}\n </div>\n </div>`;\n }).join('');\n}\n\n// --- Topic selection ---\nasync function selectTopic(id) {\n state.activeTopic = id;\n state.activeTab = 'qa';\n\n // Fetch topic detail, viz, exercises, and resources in parallel\n try {\n const [topicData, viz, exercises, resources] = await Promise.all([\n api(`/api/topics/${id}`),\n api(`/api/topics/${id}/viz`).catch(() => []),\n api(`/api/topics/${id}/exercises`).catch(() => []),\n api(`/api/topics/${id}/resources`).catch(() => []),\n ]);\n\n state.topicData = topicData;\n state.topicViz = viz || [];\n state.topicExercises = exercises || [];\n state.topicResources = resources || [];\n } catch {\n state.topicData = null;\n state.topicViz = [];\n state.topicExercises = [];\n state.topicResources = [];\n }\n\n // Update sidebar active state\n document.querySelectorAll('.topic-item').forEach(el => {\n el.classList.toggle('active', parseInt(el.dataset?.topicId) === id);\n });\n\n showPage('topic');\n renderTopicDetail();\n switchTab('qa');\n}\n\nfunction renderTopicDetail() {\n const data = state.topicData;\n if (!data) return;\n\n document.getElementById('topic-name').textContent = data.name || '';\n const statusEl = document.getElementById('topic-status');\n statusEl.textContent = (data.status || '').replace('_', ' ');\n statusEl.className = `badge ${data.status || ''}`;\n document.getElementById('topic-desc').textContent = data.description || '';\n}\n\n// --- Tab switching ---\nfunction switchTab(tab) {\n state.activeTab = tab;\n\n document.querySelectorAll('#page-topic .tab-btn').forEach(btn => {\n btn.classList.toggle('active', btn.dataset.tab === tab);\n });\n\n document.querySelectorAll('#page-topic .tab-panel').forEach(panel => {\n panel.classList.toggle('active', panel.id === `tab-${tab}`);\n });\n\n // Render tab content\n if (tab === 'qa') renderQATab();\n else if (tab === 'viz') renderVizTab();\n else if (tab === 'exercises') renderExercisesTab();\n else if (tab === 'resources') renderResourcesTab();\n}\n\n// --- Q&A Tab ---\nfunction renderQATab() {\n const container = document.getElementById('tab-qa');\n if (!container) return;\n\n const entries = state.topicData?.entries || [];\n\n if (entries.length === 0) {\n container.innerHTML = `<div class=\"empty-state\"><p>No Q&A entries yet</p><p>Ask questions in Claude to see them here</p></div>`;\n return;\n }\n\n // Group entries into Q&A cards by question_id\n const questionMap = new Map();\n const groups = [];\n\n entries.forEach(e => {\n if (e.kind === 'question') {\n const group = { question: e, answers: [] };\n questionMap.set(e.id, group);\n groups.push(group);\n } else if (e.question_id && questionMap.has(e.question_id)) {\n questionMap.get(e.question_id).answers.push(e);\n } else {\n groups.push({ standalone: e });\n }\n });\n\n // marked.parse is used intentionally for markdown rendering (same as go-learn)\n container.innerHTML = groups.map(g => {\n if (g.standalone) {\n const e = g.standalone;\n return `\n <div class=\"entry-card\">\n <div class=\"entry-header\">\n <span class=\"entry-kind ${escapeHtml(e.kind)}\">${escapeHtml(e.kind)}</span>\n <span>${formatTime(e.created_at)}</span>\n </div>\n <div class=\"entry-body\">${marked.parse(e.content || '')}</div>\n </div>`;\n }\n\n const q = g.question;\n let html = `<div class=\"qa-card\">`;\n html += `\n <div class=\"qa-question\">\n <div class=\"entry-header\">\n <span class=\"entry-kind question\">question</span>\n <span>${formatTime(q.created_at)}</span>\n </div>\n <div class=\"entry-body\">${marked.parse(q.content || '')}</div>\n </div>`;\n\n g.answers.forEach(a => {\n html += `\n <div class=\"qa-answer\">\n <div class=\"entry-header\">\n <span class=\"entry-kind ${escapeHtml(a.kind)}\">${escapeHtml(a.kind)}</span>\n <span>${formatTime(a.created_at)}</span>\n </div>\n <div class=\"entry-body\">${marked.parse(a.content || '')}</div>\n </div>`;\n });\n\n html += `</div>`;\n return html;\n }).join('');\n}\n\n// --- Visualize Tab ---\nfunction renderVizTab() {\n const container = document.getElementById('tab-viz');\n if (!container) return;\n\n const vizList = state.topicViz;\n\n if (!vizList || vizList.length === 0) {\n container.innerHTML = `<div class=\"empty-state\"><p>No visualizations yet</p><p>Visualizations will appear here as you learn</p></div>`;\n return;\n }\n\n // Reset viz state\n state.vizIndex = 0;\n state.vizStep = 0;\n\n renderVizSelector(container);\n}\n\nfunction renderVizSelector(container) {\n if (!container) container = document.getElementById('tab-viz');\n if (!container) return;\n\n const vizList = state.topicViz;\n if (!vizList || vizList.length === 0) return;\n\n let html = `<div class=\"viz-selector\">`;\n vizList.forEach((v, i) => {\n html += `<button class=\"viz-select-btn ${i === state.vizIndex ? 'active' : ''}\" onclick=\"selectViz(${i})\">${escapeHtml(v.title)}</button>`;\n });\n html += `</div>`;\n html += `<div id=\"viz-stage-container\"></div>`;\n\n container.innerHTML = html;\n renderVizStage();\n}\n\nfunction selectViz(index) {\n state.vizIndex = index;\n state.vizStep = 0;\n\n // Update selector buttons\n document.querySelectorAll('.viz-select-btn').forEach((btn, i) => {\n btn.classList.toggle('active', i === index);\n });\n\n renderVizStage();\n}\n\nfunction renderVizStage() {\n const stageContainer = document.getElementById('viz-stage-container');\n if (!stageContainer) return;\n\n const viz = state.topicViz[state.vizIndex];\n if (!viz) return;\n\n let steps;\n try {\n steps = typeof viz.steps_json === 'string' ? JSON.parse(viz.steps_json) : viz.steps_json;\n } catch {\n stageContainer.innerHTML = `<div class=\"empty-state\"><p>Invalid visualization data</p></div>`;\n return;\n }\n\n if (!steps || steps.length === 0) {\n stageContainer.innerHTML = `<div class=\"empty-state\"><p>No steps in this visualization</p></div>`;\n return;\n }\n\n const step = steps[state.vizStep] || steps[0];\n const totalSteps = steps.length;\n\n // sanitizeVizHtml strips dangerous content, allowing only safe tags with class/style\n stageContainer.innerHTML = `\n <div class=\"viz-stage\">\n <div class=\"viz-canvas\">${sanitizeVizHtml(step.html || step.canvas || '')}</div>\n ${step.description || step.desc ? `<div class=\"viz-description\">${sanitizeVizHtml(step.description || step.desc || '')}</div>` : ''}\n <div class=\"viz-controls\">\n <button onclick=\"vizPrev()\" ${state.vizStep === 0 ? 'disabled' : ''}>Prev</button>\n <span class=\"viz-step-label\">Step ${state.vizStep + 1} / ${totalSteps}</span>\n <button onclick=\"vizNext()\" ${state.vizStep >= totalSteps - 1 ? 'disabled' : ''}>Next</button>\n </div>\n </div>`;\n}\n\nfunction vizPrev() {\n if (state.vizStep > 0) {\n state.vizStep--;\n renderVizStage();\n }\n}\n\nfunction vizNext() {\n const viz = state.topicViz[state.vizIndex];\n if (!viz) return;\n let steps;\n try {\n steps = typeof viz.steps_json === 'string' ? JSON.parse(viz.steps_json) : viz.steps_json;\n } catch { return; }\n if (state.vizStep < (steps?.length || 1) - 1) {\n state.vizStep++;\n renderVizStage();\n }\n}\n\n// --- Exercises Tab ---\nfunction renderExercisesTab() {\n const container = document.getElementById('tab-exercises');\n if (!container) return;\n\n const exercises = state.topicExercises;\n\n if (!exercises || exercises.length === 0) {\n container.innerHTML = `<div class=\"empty-state\"><p>No exercises yet</p><p>Exercises are generated when you complete topics</p></div>`;\n return;\n }\n\n container.innerHTML = exercises.map((ex, i) => {\n const results = ex.results || [];\n const passed = results.filter(r => r.passed).length;\n const total = results.length;\n const hasPassed = total > 0 && passed === total;\n\n let detailHtml = '';\n\n // Quiz type\n if (ex.type === 'quiz' && ex.quiz_json) {\n let quiz;\n try {\n quiz = typeof ex.quiz_json === 'string' ? JSON.parse(ex.quiz_json) : ex.quiz_json;\n } catch { quiz = null; }\n\n if (quiz && Array.isArray(quiz)) {\n detailHtml += `<h4>Questions</h4>`;\n detailHtml += quiz.map((q, qi) => `\n <div class=\"quiz-question\" data-exercise=\"${i}\" data-question=\"${qi}\">\n <p>${marked.parse(q.question || q.text || '')}</p>\n ${(q.options || q.choices || []).map((opt, oi) => `\n <div class=\"quiz-option\" data-exercise=\"${i}\" data-question=\"${qi}\" data-option=\"${oi}\" onclick=\"selectQuizOption(this)\">\n ${escapeHtml(opt)}\n </div>\n `).join('')}\n </div>\n `).join('');\n detailHtml += `\n <div class=\"exercise-actions\">\n <button class=\"exercise-action-btn btn-primary\" onclick=\"submitQuiz(${ex.id}, ${i})\">Submit Answers</button>\n </div>`;\n }\n }\n\n // Coding type — test cases\n if (ex.type === 'coding' || ex.type === 'project' || ex.type === 'assignment') {\n if (results.length > 0) {\n detailHtml += `<h4>Test Results</h4>`;\n detailHtml += results.map(r => `\n <div class=\"test-case\">\n <div class=\"test-case-header\">\n <span class=\"test-status ${r.passed ? 'pass' : 'fail'}\"></span>\n ${escapeHtml(r.test_name)}\n </div>\n ${r.output ? `<div class=\"test-case-body\">${truncate(r.output, 300)}</div>` : ''}\n </div>\n `).join('');\n\n detailHtml += `\n <div class=\"exercise-progress\">\n <span>${passed}/${total} tests</span>\n <div class=\"exercise-progress-bar\">\n <div class=\"exercise-progress-fill ${hasPassed ? 'green' : 'yellow'}\" style=\"width:${total > 0 ? Math.round(passed / total * 100) : 0}%\"></div>\n </div>\n </div>`;\n }\n\n detailHtml += `\n <div class=\"exercise-actions\">\n <button class=\"exercise-action-btn btn-primary\" onclick=\"runExercise(${ex.id}, ${i})\">Run Tests</button>\n </div>`;\n }\n\n return `\n <div class=\"exercise-card expandable\" id=\"exercise-${i}\">\n <div class=\"exercise-header\" onclick=\"toggleExercise(${i})\">\n <span class=\"exercise-title\">${escapeHtml(ex.title)}</span>\n <span class=\"exercise-type ${escapeHtml(ex.type)}\">${escapeHtml(ex.type)}</span>\n <span class=\"exercise-expand-icon\">▼</span>\n </div>\n <div class=\"exercise-desc\">${escapeHtml(ex.description || '')}</div>\n <div class=\"exercise-meta\">\n ${ex.difficulty ? `<span>Difficulty: ${escapeHtml(ex.difficulty)}</span>` : ''}\n ${ex.est_minutes ? `<span>${ex.est_minutes} min</span>` : ''}\n ${ex.source ? `<span>Source: ${escapeHtml(ex.source)}</span>` : ''}\n ${ex.status ? `<span>Status: ${escapeHtml(ex.status)}</span>` : ''}\n </div>\n <div class=\"exercise-detail\" id=\"exercise-detail-${i}\">\n ${detailHtml}\n </div>\n </div>`;\n }).join('');\n}\n\nfunction toggleExercise(index) {\n const detail = document.getElementById('exercise-detail-' + index);\n const card = detail ? detail.closest('.exercise-card') : null;\n if (detail) detail.classList.toggle('open');\n if (card) card.classList.toggle('open');\n}\n\nfunction selectQuizOption(el) {\n const questionEl = el.closest('.quiz-question');\n if (questionEl) {\n questionEl.querySelectorAll('.quiz-option').forEach(opt => opt.classList.remove('selected'));\n }\n el.classList.add('selected');\n}\n\nasync function submitQuiz(exerciseId, cardIndex) {\n const card = document.getElementById(`exercise-${cardIndex}`);\n if (!card) return;\n\n const answers = [];\n card.querySelectorAll('.quiz-question').forEach(q => {\n const selected = q.querySelector('.quiz-option.selected');\n if (selected) {\n answers.push(parseInt(selected.dataset.option));\n } else {\n answers.push(-1);\n }\n });\n\n try {\n const result = await fetch(`/api/exercises/${exerciseId}/submit`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ answers }),\n });\n const data = await result.json();\n\n if (data.results) {\n data.results.forEach((r, i) => {\n const questionEl = card.querySelectorAll('.quiz-question')[i];\n if (!questionEl) return;\n questionEl.querySelectorAll('.quiz-option').forEach((opt, oi) => {\n opt.classList.remove('selected');\n if (oi === r.correct_index) opt.classList.add('correct');\n else if (oi === answers[i] && !r.passed) opt.classList.add('incorrect');\n });\n });\n }\n\n if (data.score !== undefined) {\n const actionsEl = card.querySelector('.exercise-actions');\n if (actionsEl) {\n const scoreDiv = document.createElement('div');\n scoreDiv.style.cssText = `margin-top:8px;font-size:14px;font-weight:600;color:${data.passed ? 'var(--green)' : 'var(--yellow)'}`;\n scoreDiv.textContent = `Score: ${data.score}/${data.total}${data.passed ? ' - Passed!' : ''}`;\n actionsEl.appendChild(scoreDiv);\n }\n }\n } catch (err) {\n console.error('Submit quiz error:', err);\n }\n}\n\nasync function runExercise(exerciseId, cardIndex) {\n const btn = document.querySelector(`#exercise-${cardIndex} .btn-primary`);\n if (btn) { btn.textContent = 'Running...'; btn.disabled = true; }\n\n try {\n const res = await fetch(`/api/exercises/${exerciseId}/run`, { method: 'POST' });\n const data = await res.json();\n\n if (data.results && state.topicExercises[cardIndex]) {\n state.topicExercises[cardIndex].results = data.results;\n }\n\n renderExercisesTab();\n\n // Re-open the card\n const detail = document.getElementById(`exercise-detail-${cardIndex}`);\n if (detail) detail.classList.add('open');\n } catch (err) {\n console.error('Run exercise error:', err);\n if (btn) { btn.textContent = 'Run Tests'; btn.disabled = false; }\n }\n}\n\n// --- Resources Tab ---\nfunction renderResourcesTab() {\n const container = document.getElementById('tab-resources');\n if (!container) return;\n\n const resources = state.topicResources || [];\n\n if (resources.length === 0) {\n 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>';\n return;\n }\n\n let html = '<div class=\"resources-list\">';\n for (const r of resources) {\n const isFile = r.url.startsWith('file://');\n const isPdf = isFile && r.url.toLowerCase().endsWith('.pdf');\n\n if (isPdf) {\n html += '<div class=\"resource-card resource-pdf\">' +\n '<div class=\"resource-pdf-header\" data-resource-id=\"' + r.id + '\">' +\n '<span class=\"resource-title\">' + escapeHtml(r.title) + '</span>' +\n '<span class=\"resource-badge\">PDF</span>' +\n '<span class=\"resource-chevron\">▼</span>' +\n '</div>' +\n '<div class=\"resource-pdf-viewer\" id=\"pdf-viewer-' + r.id + '\" style=\"display:none;\">' +\n '<iframe data-src=\"/api/resources/' + r.id + '/file\" type=\"application/pdf\"></iframe>' +\n '</div>' +\n '</div>';\n } else if (isFile) {\n html += '<div class=\"resource-card\">' +\n '<span class=\"resource-title\">' + escapeHtml(r.title) + '</span>' +\n '<span class=\"resource-url\">' + escapeHtml(r.url) + '</span>' +\n '</div>';\n } else {\n html += '<a href=\"' + escapeHtml(r.url) + '\" target=\"_blank\" rel=\"noopener\" class=\"resource-card\">' +\n '<span class=\"resource-title\">' + escapeHtml(r.title) + '</span>' +\n '<span class=\"resource-url\">' + escapeHtml(r.url) + '</span>' +\n '</a>';\n }\n }\n html += '</div>';\n\n container.innerHTML = html;\n\n // Attach expand/collapse handlers for PDF viewers (lazy-load on first expand)\n container.querySelectorAll('.resource-pdf-header').forEach(header => {\n header.addEventListener('click', () => {\n const id = header.dataset.resourceId;\n const viewer = document.getElementById('pdf-viewer-' + id);\n const chevron = header.querySelector('.resource-chevron');\n if (viewer) {\n const isOpen = viewer.style.display !== 'none';\n viewer.style.display = isOpen ? 'none' : 'block';\n if (chevron) chevron.classList.toggle('open', !isOpen);\n // Lazy-load: set iframe src on first expand\n const iframe = viewer.querySelector('iframe');\n if (!isOpen && iframe && !iframe.src) {\n iframe.src = iframe.dataset.src;\n }\n }\n });\n });\n}\n\n// --- Navigation ---\nfunction showPage(page) {\n document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));\n\n const target = document.getElementById(`page-${page}`);\n if (target) target.classList.add('active');\n\n document.querySelectorAll('.nav-btn').forEach(btn => {\n btn.classList.toggle('active', btn.dataset.page === page);\n });\n\n if (page === 'home') renderHome();\n else if (page === 'topics') renderTopicsPage();\n else if (page === 'search') document.getElementById('search-input')?.focus();\n}\n\n// --- Search ---\nconst searchInput = document.getElementById('search-input');\nif (searchInput) {\n searchInput.addEventListener('input', (e) => {\n clearTimeout(state.searchTimeout);\n state.searchTimeout = setTimeout(() => doSearch(e.target.value, 'search-results'), 200);\n });\n}\n\nconst modalSearchInput = document.getElementById('modal-search-input');\nif (modalSearchInput) {\n modalSearchInput.addEventListener('input', (e) => {\n clearTimeout(state.searchTimeout);\n state.searchTimeout = setTimeout(() => doSearch(e.target.value, 'modal-search-results'), 200);\n });\n}\n\nasync function doSearch(query, resultsContainerId) {\n const container = document.getElementById(resultsContainerId);\n if (!container) return;\n\n if (!query || !query.trim()) {\n container.innerHTML = '';\n return;\n }\n\n try {\n const results = await api(`/api/search?q=${encodeURIComponent(query)}`);\n\n if (!results || results.length === 0) {\n container.innerHTML = '<div class=\"search-no-results\">No results found</div>';\n return;\n }\n\n container.innerHTML = results.map(r => `\n <div class=\"search-result-item\" onclick=\"closeSearchModal(); selectTopic(${r.topic_id})\">\n <div class=\"search-result-meta\">\n <span class=\"entry-kind ${escapeHtml(r.kind)}\">${escapeHtml(r.kind)}</span>\n </div>\n <div class=\"search-result-content\">${truncate(r.content, 150)}</div>\n </div>\n `).join('');\n } catch {\n container.innerHTML = '<div class=\"search-no-results\">Search failed</div>';\n }\n}\n\nfunction openSearchModal() {\n const modal = document.getElementById('search-modal');\n if (modal) {\n modal.classList.remove('hidden');\n const input = document.getElementById('modal-search-input');\n if (input) { input.value = ''; input.focus(); }\n const results = document.getElementById('modal-search-results');\n if (results) results.innerHTML = '';\n }\n}\n\nfunction closeSearchModal() {\n const modal = document.getElementById('search-modal');\n if (modal) modal.classList.add('hidden');\n}\n\n// --- Keyboard shortcuts ---\ndocument.addEventListener('keydown', (e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n openSearchModal();\n }\n if (e.key === 'Escape') {\n closeSearchModal();\n }\n});\n";
|
|
23839
|
+
var appJs = "// StudyDash — Dashboard Application\n// Connects to the learn-cc API and renders a responsive learning dashboard.\n\n// --- Markdown config ---\nmarked.setOptions({\n highlight: function(code, lang) {\n if (lang && hljs.getLanguage(lang)) {\n return hljs.highlight(code, { language: lang }).value;\n }\n return hljs.highlightAuto(code).value;\n },\n breaks: true,\n gfm: true,\n});\n\n// --- State ---\nconst state = {\n subjects: [],\n activeSubject: null,\n phases: [],\n activeTopic: null,\n activeTab: 'qa',\n topicData: null,\n topicViz: [],\n topicExercises: [],\n topicResources: [],\n searchTimeout: null,\n vizIndex: 0,\n vizStep: 0,\n editorExercise: null,\n editorActiveTab: 'main',\n editorMainContent: '',\n editorTestContent: '',\n editorView: null,\n};\n\n// --- API Helper ---\nasync function api(path) {\n const res = await fetch(path);\n if (!res.ok) throw new Error(`API error: ${res.status}`);\n return res.json();\n}\n\n// --- Sanitize viz HTML ---\nfunction sanitizeVizHtml(html) {\n const allowed = ['div', 'span', 'small', 'br', 'code', 'strong', 'em'];\n const tmp = document.createElement('div');\n tmp.innerHTML = html;\n // Remove all script tags\n tmp.querySelectorAll('script').forEach(el => el.remove());\n // Walk all elements, remove disallowed tags, strip non-class/style attributes\n const walk = (node) => {\n const children = [...node.children];\n for (const child of children) {\n if (!allowed.includes(child.tagName.toLowerCase())) {\n child.replaceWith(...child.childNodes);\n } else {\n [...child.attributes].forEach(attr => {\n if (attr.name !== 'class' && attr.name !== 'style') child.removeAttribute(attr.name);\n });\n walk(child);\n }\n }\n };\n walk(tmp);\n return tmp.innerHTML;\n}\n\n// --- Escape HTML for text content in templates ---\nfunction escapeHtml(str) {\n if (!str) return '';\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n}\n\n// --- Format time ---\nfunction formatTime(iso) {\n if (!iso) return '';\n const d = new Date(iso);\n return d.toLocaleDateString('en-US', {\n month: 'short', day: 'numeric', year: 'numeric',\n hour: '2-digit', minute: '2-digit',\n });\n}\n\nfunction truncate(text, max) {\n if (!text) return '';\n if (text.length <= max) return escapeHtml(text);\n return escapeHtml(text.substring(0, max)) + '...';\n}\n\n// --- On load ---\ndocument.addEventListener('DOMContentLoaded', async () => {\n try {\n state.subjects = await api('/api/subjects');\n } catch {\n state.subjects = [];\n }\n\n if (state.subjects.length > 0) {\n state.activeSubject = state.subjects[0];\n await loadSubject();\n }\n\n renderAllSubjectSwitchers();\n renderHome();\n connectSSE();\n});\n\n// --- SSE ---\nfunction connectSSE() {\n const dot = document.getElementById('sse-dot');\n let evtSource;\n\n function connect() {\n evtSource = new EventSource('/api/events');\n\n evtSource.onopen = () => {\n if (dot) { dot.classList.remove('disconnected'); dot.classList.add('connected'); }\n };\n\n evtSource.onmessage = async (event) => {\n try {\n const data = JSON.parse(event.data);\n if (data.type === 'update') {\n await refresh();\n }\n } catch { /* ignore parse errors */ }\n };\n\n evtSource.onerror = () => {\n if (dot) { dot.classList.remove('connected'); dot.classList.add('disconnected'); }\n evtSource.close();\n setTimeout(connect, 3000);\n };\n }\n\n connect();\n}\n\nasync function refresh() {\n // Re-fetch subjects\n try {\n state.subjects = await api('/api/subjects');\n } catch { /* keep existing */ }\n\n if (state.activeSubject) {\n // Refresh the active subject from the new list\n const updated = state.subjects.find(s => s.id === state.activeSubject.id);\n if (updated) state.activeSubject = updated;\n await loadSubject();\n }\n\n renderAllSubjectSwitchers();\n\n // Re-render current view\n const activePage = document.querySelector('.page.active');\n if (activePage) {\n const pageId = activePage.id.replace('page-', '');\n if (pageId === 'home') renderHome();\n else if (pageId === 'topics') renderTopicsPage();\n else if (pageId === 'topic' && state.activeTopic) await selectTopic(state.activeTopic);\n }\n}\n\n// --- Subject management ---\nasync function loadSubject() {\n if (!state.activeSubject) return;\n try {\n state.phases = await api(`/api/subjects/${state.activeSubject.id}/phases`);\n } catch {\n state.phases = [];\n }\n renderSidebar();\n}\n\nasync function switchSubject(id) {\n const subject = state.subjects.find(s => s.id === id);\n if (!subject) return;\n state.activeSubject = subject;\n state.activeTopic = null;\n state.topicData = null;\n await loadSubject();\n renderAllSubjectSwitchers();\n renderHome();\n renderTopicsPage();\n // If on topic detail page, go back to topics\n const activePage = document.querySelector('.page.active');\n if (activePage && activePage.id === 'page-topic') {\n showPage('topics');\n }\n}\n\nfunction renderAllSubjectSwitchers() {\n const containers = ['home-subjects', 'topics-subjects', 'sidebar-subjects'];\n containers.forEach(id => {\n const el = document.getElementById(id);\n if (!el) return;\n if (state.subjects.length === 0) {\n el.innerHTML = '';\n return;\n }\n el.innerHTML = state.subjects.map(s =>\n `<button class=\"subject-btn ${state.activeSubject && state.activeSubject.id === s.id ? 'active' : ''}\"\n onclick=\"switchSubject(${s.id})\">${escapeHtml(s.name)}</button>`\n ).join('');\n });\n}\n\n// --- Progress ---\nfunction renderProgress() {\n if (!state.activeSubject) return;\n const p = state.activeSubject.progress || {};\n const total = p.total_topics || 0;\n const done = p.done || 0;\n const pct = total > 0 ? Math.round((done / total) * 100) : 0;\n const text = `${done} / ${total} topics`;\n\n // Home progress\n const hFill = document.getElementById('home-progress-fill');\n const hText = document.getElementById('home-progress-text');\n if (hFill) hFill.style.width = pct + '%';\n if (hText) hText.textContent = text;\n\n // Sidebar progress\n const sFill = document.getElementById('sidebar-progress-fill');\n const sText = document.getElementById('sidebar-progress-text');\n if (sFill) sFill.style.width = pct + '%';\n if (sText) sText.textContent = text;\n}\n\n// --- Sidebar ---\nfunction renderSidebar() {\n renderProgress();\n const container = document.getElementById('sidebar-phases');\n if (!container) return;\n\n if (state.phases.length === 0) {\n container.innerHTML = '<div class=\"empty-state\"><p>No phases yet</p></div>';\n return;\n }\n\n container.innerHTML = state.phases.map(phase => {\n const topics = phase.topics || [];\n const doneCount = topics.filter(t => t.status === 'done').length;\n return `\n <div class=\"phase-group\">\n <div class=\"phase-header\" onclick=\"togglePhase(this)\">\n ${escapeHtml(phase.name)}\n <span style=\"font-size:10px;color:var(--text-muted)\">${doneCount}/${topics.length}</span>\n <span class=\"chevron\">▼</span>\n </div>\n <div class=\"phase-topics\">\n ${topics.map(t => `\n <div class=\"topic-item ${state.activeTopic === t.id ? 'active' : ''}\"\n onclick=\"selectTopic(${t.id})\"\n data-topic-id=\"${t.id}\">\n <span class=\"status-dot ${escapeHtml(t.status)}\"></span>\n <span>${escapeHtml(t.name)}</span>\n </div>\n `).join('')}\n </div>\n </div>`;\n }).join('');\n}\n\nfunction togglePhase(el) {\n el.classList.toggle('collapsed');\n el.nextElementSibling.classList.toggle('collapsed');\n}\n\n// --- Home page ---\nfunction renderHome() {\n renderProgress();\n\n const statsEl = document.getElementById('home-stats');\n const recentEl = document.getElementById('home-recent');\n\n if (state.subjects.length === 0) {\n if (statsEl) statsEl.innerHTML = '';\n if (recentEl) recentEl.innerHTML = `\n <div class=\"empty-state\">\n <p>Welcome to StudyDash!</p>\n <p>Start by creating a subject with <code>/learn</code></p>\n </div>`;\n return;\n }\n\n const p = state.activeSubject ? (state.activeSubject.progress || {}) : {};\n\n if (statsEl) {\n statsEl.innerHTML = `\n <div class=\"stat-card\">\n <div class=\"stat-value\">${p.done || 0}</div>\n <div class=\"stat-label\">Topics Done</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-value green\">${p.total_entries || 0}</div>\n <div class=\"stat-label\">Q&A Entries</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-value yellow\">${p.total_exercises || 0}</div>\n <div class=\"stat-label\">Exercises</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-value purple\">${p.total_viz || 0}</div>\n <div class=\"stat-label\">Visualizations</div>\n </div>`;\n }\n\n // Show recently active topics (in_progress first, then by updated_at)\n if (recentEl) {\n const allTopics = state.phases.flatMap(ph => (ph.topics || []).map(t => ({ ...t, phaseName: ph.name })));\n const active = allTopics\n .filter(t => t.status !== 'todo')\n .sort((a, b) => {\n if (a.status === 'in_progress' && b.status !== 'in_progress') return -1;\n if (b.status === 'in_progress' && a.status !== 'in_progress') return 1;\n return new Date(b.updated_at || 0) - new Date(a.updated_at || 0);\n })\n .slice(0, 5);\n\n if (active.length === 0) {\n recentEl.innerHTML = `<div class=\"empty-state\"><p>No active topics yet. Import a curriculum with <code>/learn import</code></p></div>`;\n } else {\n recentEl.innerHTML = active.map(t => `\n <div class=\"exercise-card\" style=\"cursor:pointer\" onclick=\"selectTopic(${t.id})\">\n <div class=\"exercise-header\">\n <span class=\"exercise-title\">${escapeHtml(t.name)}</span>\n <span class=\"badge ${escapeHtml(t.status)}\">${escapeHtml(t.status.replace('_', ' '))}</span>\n </div>\n <div class=\"exercise-desc\">${escapeHtml(t.phaseName)}</div>\n </div>\n `).join('');\n }\n }\n}\n\n// --- Topics page ---\nfunction renderTopicsPage() {\n const container = document.getElementById('topics-phases');\n if (!container) return;\n\n if (state.phases.length === 0) {\n container.innerHTML = `<div class=\"empty-state\"><p>No topics yet.</p><p>Import a curriculum with <code>/learn import</code></p></div>`;\n return;\n }\n\n container.innerHTML = state.phases.map(phase => {\n const topics = phase.topics || [];\n return `\n <div class=\"phase-group\">\n <div class=\"phase-header\" onclick=\"togglePhase(this)\">\n ${escapeHtml(phase.name)}\n <span class=\"chevron\">▼</span>\n </div>\n <div class=\"phase-topics\">\n ${topics.map(t => `\n <div class=\"topic-item ${state.activeTopic === t.id ? 'active' : ''}\"\n onclick=\"selectTopic(${t.id})\">\n <span class=\"status-dot ${escapeHtml(t.status)}\"></span>\n ${escapeHtml(t.name)}\n <span class=\"topic-count\"></span>\n </div>\n `).join('')}\n </div>\n </div>`;\n }).join('');\n}\n\n// --- Topic selection ---\nasync function selectTopic(id) {\n state.activeTopic = id;\n state.activeTab = 'qa';\n\n // Fetch topic detail, viz, exercises, and resources in parallel\n try {\n const [topicData, viz, exercises, resources] = await Promise.all([\n api(`/api/topics/${id}`),\n api(`/api/topics/${id}/viz`).catch(() => []),\n api(`/api/topics/${id}/exercises`).catch(() => []),\n api(`/api/topics/${id}/resources`).catch(() => []),\n ]);\n\n state.topicData = topicData;\n state.topicViz = viz || [];\n state.topicExercises = exercises || [];\n state.topicResources = resources || [];\n } catch {\n state.topicData = null;\n state.topicViz = [];\n state.topicExercises = [];\n state.topicResources = [];\n }\n\n // Update sidebar active state\n document.querySelectorAll('.topic-item').forEach(el => {\n el.classList.toggle('active', parseInt(el.dataset?.topicId) === id);\n });\n\n showPage('topic');\n renderTopicDetail();\n switchTab('qa');\n}\n\nfunction renderTopicDetail() {\n const data = state.topicData;\n if (!data) return;\n\n document.getElementById('topic-name').textContent = data.name || '';\n const statusEl = document.getElementById('topic-status');\n statusEl.textContent = (data.status || '').replace('_', ' ');\n statusEl.className = `badge ${data.status || ''}`;\n document.getElementById('topic-desc').textContent = data.description || '';\n}\n\n// --- Tab switching ---\nfunction switchTab(tab) {\n state.activeTab = tab;\n\n document.querySelectorAll('#page-topic .tab-btn').forEach(btn => {\n btn.classList.toggle('active', btn.dataset.tab === tab);\n });\n\n document.querySelectorAll('#page-topic .tab-panel').forEach(panel => {\n panel.classList.toggle('active', panel.id === `tab-${tab}`);\n });\n\n // Render tab content\n if (tab === 'qa') renderQATab();\n else if (tab === 'viz') renderVizTab();\n else if (tab === 'exercises') renderExercisesTab();\n else if (tab === 'resources') renderResourcesTab();\n}\n\n// --- Q&A Tab ---\nfunction renderQATab() {\n const container = document.getElementById('tab-qa');\n if (!container) return;\n\n const entries = state.topicData?.entries || [];\n\n if (entries.length === 0) {\n container.innerHTML = `<div class=\"empty-state\"><p>No Q&A entries yet</p><p>Ask questions in Claude to see them here</p></div>`;\n return;\n }\n\n // Group entries into Q&A cards by question_id\n const questionMap = new Map();\n const groups = [];\n\n entries.forEach(e => {\n if (e.kind === 'question') {\n const group = { question: e, answers: [] };\n questionMap.set(e.id, group);\n groups.push(group);\n } else if (e.question_id && questionMap.has(e.question_id)) {\n questionMap.get(e.question_id).answers.push(e);\n } else {\n groups.push({ standalone: e });\n }\n });\n\n // marked.parse is used intentionally for markdown rendering (same as go-learn)\n container.innerHTML = groups.map(g => {\n if (g.standalone) {\n const e = g.standalone;\n return `\n <div class=\"entry-card\">\n <div class=\"entry-header\">\n <span class=\"entry-kind ${escapeHtml(e.kind)}\">${escapeHtml(e.kind)}</span>\n <span>${formatTime(e.created_at)}</span>\n </div>\n <div class=\"entry-body\">${marked.parse(e.content || '')}</div>\n </div>`;\n }\n\n const q = g.question;\n let html = `<div class=\"qa-card\">`;\n html += `\n <div class=\"qa-question\">\n <div class=\"entry-header\">\n <span class=\"entry-kind question\">question</span>\n <span>${formatTime(q.created_at)}</span>\n </div>\n <div class=\"entry-body\">${marked.parse(q.content || '')}</div>\n </div>`;\n\n g.answers.forEach(a => {\n html += `\n <div class=\"qa-answer\">\n <div class=\"entry-header\">\n <span class=\"entry-kind ${escapeHtml(a.kind)}\">${escapeHtml(a.kind)}</span>\n <span>${formatTime(a.created_at)}</span>\n </div>\n <div class=\"entry-body\">${marked.parse(a.content || '')}</div>\n </div>`;\n });\n\n html += `</div>`;\n return html;\n }).join('');\n}\n\n// --- Visualize Tab ---\nfunction renderVizTab() {\n const container = document.getElementById('tab-viz');\n if (!container) return;\n\n const vizList = state.topicViz;\n\n if (!vizList || vizList.length === 0) {\n container.innerHTML = `<div class=\"empty-state\"><p>No visualizations yet</p><p>Visualizations will appear here as you learn</p></div>`;\n return;\n }\n\n // Reset viz state\n state.vizIndex = 0;\n state.vizStep = 0;\n\n renderVizSelector(container);\n}\n\nfunction renderVizSelector(container) {\n if (!container) container = document.getElementById('tab-viz');\n if (!container) return;\n\n const vizList = state.topicViz;\n if (!vizList || vizList.length === 0) return;\n\n let html = `<div class=\"viz-selector\">`;\n vizList.forEach((v, i) => {\n html += `<button class=\"viz-select-btn ${i === state.vizIndex ? 'active' : ''}\" onclick=\"selectViz(${i})\">${escapeHtml(v.title)}</button>`;\n });\n html += `</div>`;\n html += `<div id=\"viz-stage-container\"></div>`;\n\n container.innerHTML = html;\n renderVizStage();\n}\n\nfunction selectViz(index) {\n state.vizIndex = index;\n state.vizStep = 0;\n\n // Update selector buttons\n document.querySelectorAll('.viz-select-btn').forEach((btn, i) => {\n btn.classList.toggle('active', i === index);\n });\n\n renderVizStage();\n}\n\nfunction renderVizStage() {\n const stageContainer = document.getElementById('viz-stage-container');\n if (!stageContainer) return;\n\n const viz = state.topicViz[state.vizIndex];\n if (!viz) return;\n\n let steps;\n try {\n steps = typeof viz.steps_json === 'string' ? JSON.parse(viz.steps_json) : viz.steps_json;\n } catch {\n stageContainer.innerHTML = `<div class=\"empty-state\"><p>Invalid visualization data</p></div>`;\n return;\n }\n\n if (!steps || steps.length === 0) {\n stageContainer.innerHTML = `<div class=\"empty-state\"><p>No steps in this visualization</p></div>`;\n return;\n }\n\n const step = steps[state.vizStep] || steps[0];\n const totalSteps = steps.length;\n\n // sanitizeVizHtml strips dangerous content, allowing only safe tags with class/style\n stageContainer.innerHTML = `\n <div class=\"viz-stage\">\n <div class=\"viz-canvas\">${sanitizeVizHtml(step.html || step.canvas || '')}</div>\n ${step.description || step.desc ? `<div class=\"viz-description\">${sanitizeVizHtml(step.description || step.desc || '')}</div>` : ''}\n <div class=\"viz-controls\">\n <button onclick=\"vizPrev()\" ${state.vizStep === 0 ? 'disabled' : ''}>Prev</button>\n <span class=\"viz-step-label\">Step ${state.vizStep + 1} / ${totalSteps}</span>\n <button onclick=\"vizNext()\" ${state.vizStep >= totalSteps - 1 ? 'disabled' : ''}>Next</button>\n </div>\n </div>`;\n}\n\nfunction vizPrev() {\n if (state.vizStep > 0) {\n state.vizStep--;\n renderVizStage();\n }\n}\n\nfunction vizNext() {\n const viz = state.topicViz[state.vizIndex];\n if (!viz) return;\n let steps;\n try {\n steps = typeof viz.steps_json === 'string' ? JSON.parse(viz.steps_json) : viz.steps_json;\n } catch { return; }\n if (state.vizStep < (steps?.length || 1) - 1) {\n state.vizStep++;\n renderVizStage();\n }\n}\n\n// --- Exercises Tab ---\nfunction renderExercisesTab() {\n const container = document.getElementById('tab-exercises');\n if (!container) return;\n\n const exercises = state.topicExercises;\n\n if (!exercises || exercises.length === 0) {\n container.innerHTML = `<div class=\"empty-state\"><p>No exercises yet</p><p>Exercises are generated when you complete topics</p></div>`;\n return;\n }\n\n container.innerHTML = exercises.map((ex, i) => {\n const results = ex.results || [];\n const passed = results.filter(r => r.passed).length;\n const total = results.length;\n const hasPassed = total > 0 && passed === total;\n\n let detailHtml = '';\n\n // Quiz type\n if (ex.type === 'quiz' && ex.quiz_json) {\n let quiz;\n try {\n quiz = typeof ex.quiz_json === 'string' ? JSON.parse(ex.quiz_json) : ex.quiz_json;\n } catch { quiz = null; }\n\n if (quiz && Array.isArray(quiz)) {\n detailHtml += `<h4>Questions</h4>`;\n detailHtml += quiz.map((q, qi) => `\n <div class=\"quiz-question\" data-exercise=\"${i}\" data-question=\"${qi}\">\n <p>${marked.parse(q.question || q.text || '')}</p>\n ${(q.options || q.choices || []).map((opt, oi) => `\n <div class=\"quiz-option\" data-exercise=\"${i}\" data-question=\"${qi}\" data-option=\"${oi}\" onclick=\"selectQuizOption(this)\">\n ${escapeHtml(opt)}\n </div>\n `).join('')}\n </div>\n `).join('');\n detailHtml += `\n <div class=\"exercise-actions\">\n <button class=\"exercise-action-btn btn-primary\" onclick=\"submitQuiz(${ex.id}, ${i})\">Submit Answers</button>\n </div>`;\n }\n }\n\n // Coding type — test cases\n if (ex.type === 'coding' || ex.type === 'project' || ex.type === 'assignment') {\n if (results.length > 0) {\n detailHtml += `<h4>Test Results</h4>`;\n detailHtml += results.map(r => `\n <div class=\"test-case\">\n <div class=\"test-case-header\">\n <span class=\"test-status ${r.passed ? 'pass' : 'fail'}\"></span>\n ${escapeHtml(r.test_name)}\n </div>\n ${r.output ? `<div class=\"test-case-body\">${truncate(r.output, 300)}</div>` : ''}\n </div>\n `).join('');\n\n detailHtml += `\n <div class=\"exercise-progress\">\n <span>${passed}/${total} tests</span>\n <div class=\"exercise-progress-bar\">\n <div class=\"exercise-progress-fill ${hasPassed ? 'green' : 'yellow'}\" style=\"width:${total > 0 ? Math.round(passed / total * 100) : 0}%\"></div>\n </div>\n </div>`;\n }\n\n detailHtml += `\n <div class=\"exercise-actions\">\n <button class=\"exercise-action-btn btn-primary\" onclick=\"openExerciseEditor(${ex.id})\">Open Editor</button>\n <button class=\"exercise-action-btn\" onclick=\"runExercise(${ex.id}, ${i})\">Run Tests</button>\n </div>`;\n }\n\n return `\n <div class=\"exercise-card expandable\" id=\"exercise-${i}\">\n <div class=\"exercise-header\" onclick=\"toggleExercise(${i})\">\n <span class=\"exercise-title\">${escapeHtml(ex.title)}</span>\n <span class=\"exercise-type ${escapeHtml(ex.type)}\">${escapeHtml(ex.type)}</span>\n <span class=\"exercise-expand-icon\">▼</span>\n </div>\n <div class=\"exercise-desc\">${escapeHtml(ex.description || '')}</div>\n <div class=\"exercise-meta\">\n ${ex.difficulty ? `<span>Difficulty: ${escapeHtml(ex.difficulty)}</span>` : ''}\n ${ex.est_minutes ? `<span>${ex.est_minutes} min</span>` : ''}\n ${ex.source ? `<span>Source: ${escapeHtml(ex.source)}</span>` : ''}\n ${ex.status ? `<span>Status: ${escapeHtml(ex.status)}</span>` : ''}\n </div>\n <div class=\"exercise-detail\" id=\"exercise-detail-${i}\">\n ${detailHtml}\n </div>\n </div>`;\n }).join('');\n}\n\nfunction toggleExercise(index) {\n const detail = document.getElementById('exercise-detail-' + index);\n const card = detail ? detail.closest('.exercise-card') : null;\n if (detail) detail.classList.toggle('open');\n if (card) card.classList.toggle('open');\n}\n\nfunction selectQuizOption(el) {\n const questionEl = el.closest('.quiz-question');\n if (questionEl) {\n questionEl.querySelectorAll('.quiz-option').forEach(opt => opt.classList.remove('selected'));\n }\n el.classList.add('selected');\n}\n\nasync function submitQuiz(exerciseId, cardIndex) {\n const card = document.getElementById(`exercise-${cardIndex}`);\n if (!card) return;\n\n const answers = [];\n card.querySelectorAll('.quiz-question').forEach(q => {\n const selected = q.querySelector('.quiz-option.selected');\n if (selected) {\n answers.push(parseInt(selected.dataset.option));\n } else {\n answers.push(-1);\n }\n });\n\n try {\n const result = await fetch(`/api/exercises/${exerciseId}/submit`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ answers }),\n });\n const data = await result.json();\n\n if (data.results) {\n data.results.forEach((r, i) => {\n const questionEl = card.querySelectorAll('.quiz-question')[i];\n if (!questionEl) return;\n questionEl.querySelectorAll('.quiz-option').forEach((opt, oi) => {\n opt.classList.remove('selected');\n if (oi === r.correct_index) opt.classList.add('correct');\n else if (oi === answers[i] && !r.passed) opt.classList.add('incorrect');\n });\n });\n }\n\n if (data.score !== undefined) {\n const actionsEl = card.querySelector('.exercise-actions');\n if (actionsEl) {\n const scoreDiv = document.createElement('div');\n scoreDiv.style.cssText = `margin-top:8px;font-size:14px;font-weight:600;color:${data.passed ? 'var(--green)' : 'var(--yellow)'}`;\n scoreDiv.textContent = `Score: ${data.score}/${data.total}${data.passed ? ' - Passed!' : ''}`;\n actionsEl.appendChild(scoreDiv);\n }\n }\n } catch (err) {\n console.error('Submit quiz error:', err);\n }\n}\n\nasync function runExercise(exerciseId, cardIndex) {\n const btn = document.querySelector(`#exercise-${cardIndex} .btn-primary`);\n if (btn) { btn.textContent = 'Running...'; btn.disabled = true; }\n\n try {\n const res = await fetch(`/api/exercises/${exerciseId}/run`, { method: 'POST' });\n const data = await res.json();\n\n if (data.results && state.topicExercises[cardIndex]) {\n state.topicExercises[cardIndex].results = data.results;\n }\n\n renderExercisesTab();\n\n // Re-open the card\n const detail = document.getElementById(`exercise-detail-${cardIndex}`);\n if (detail) detail.classList.add('open');\n } catch (err) {\n console.error('Run exercise error:', err);\n if (btn) { btn.textContent = 'Run Tests'; btn.disabled = false; }\n }\n}\n\n// --- Resources Tab ---\nfunction renderResourcesTab() {\n const container = document.getElementById('tab-resources');\n if (!container) return;\n\n const resources = state.topicResources || [];\n\n if (resources.length === 0) {\n 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>';\n return;\n }\n\n let html = '<div class=\"resources-list\">';\n for (const r of resources) {\n const isFile = r.url.startsWith('file://');\n const isPdf = isFile && r.url.toLowerCase().endsWith('.pdf');\n\n if (isPdf) {\n html += '<div class=\"resource-card resource-pdf\">' +\n '<div class=\"resource-pdf-header\" data-resource-id=\"' + r.id + '\">' +\n '<span class=\"resource-title\">' + escapeHtml(r.title) + '</span>' +\n '<span class=\"resource-badge\">PDF</span>' +\n '<span class=\"resource-chevron\">▼</span>' +\n '</div>' +\n '<div class=\"resource-pdf-viewer\" id=\"pdf-viewer-' + r.id + '\" style=\"display:none;\">' +\n '<iframe data-src=\"/api/resources/' + r.id + '/file\" type=\"application/pdf\"></iframe>' +\n '</div>' +\n '</div>';\n } else if (isFile) {\n html += '<div class=\"resource-card\">' +\n '<span class=\"resource-title\">' + escapeHtml(r.title) + '</span>' +\n '<span class=\"resource-url\">' + escapeHtml(r.url) + '</span>' +\n '</div>';\n } else {\n html += '<a href=\"' + escapeHtml(r.url) + '\" target=\"_blank\" rel=\"noopener\" class=\"resource-card\">' +\n '<span class=\"resource-title\">' + escapeHtml(r.title) + '</span>' +\n '<span class=\"resource-url\">' + escapeHtml(r.url) + '</span>' +\n '</a>';\n }\n }\n html += '</div>';\n\n container.innerHTML = html;\n\n // Attach expand/collapse handlers for PDF viewers (lazy-load on first expand)\n container.querySelectorAll('.resource-pdf-header').forEach(header => {\n header.addEventListener('click', () => {\n const id = header.dataset.resourceId;\n const viewer = document.getElementById('pdf-viewer-' + id);\n const chevron = header.querySelector('.resource-chevron');\n if (viewer) {\n const isOpen = viewer.style.display !== 'none';\n viewer.style.display = isOpen ? 'none' : 'block';\n if (chevron) chevron.classList.toggle('open', !isOpen);\n // Lazy-load: set iframe src on first expand\n const iframe = viewer.querySelector('iframe');\n if (!isOpen && iframe && !iframe.src) {\n iframe.src = iframe.dataset.src;\n }\n }\n });\n });\n}\n\n// --- CodeMirror Lazy Loading ---\nasync function loadCodeMirror() {\n if (window._cm) return window._cm;\n\n const [\n { EditorView, basicSetup },\n { EditorState },\n { go },\n { oneDark },\n { keymap },\n ] = await Promise.all([\n import('https://esm.sh/codemirror@6.65.7'),\n import('https://esm.sh/@codemirror/state@6.5.2'),\n import('https://esm.sh/@codemirror/lang-go@6.0.1'),\n import('https://esm.sh/@codemirror/theme-one-dark@6.1.2'),\n import('https://esm.sh/@codemirror/view@6.36.5'),\n ]);\n\n window._cm = { EditorView, EditorState, basicSetup, go, oneDark, keymap };\n return window._cm;\n}\n\n// --- Exercise Editor ---\nasync function openExerciseEditor(exerciseId) {\n const [exerciseList, files] = await Promise.all([\n api(`/api/topics/${state.activeTopic}/exercises`),\n api(`/api/exercises/${exerciseId}/files`),\n ]);\n\n const exercise = exerciseList.find(e => e.id === exerciseId);\n if (!exercise || !files) return;\n\n state.editorExercise = exercise;\n state.editorMainContent = files.main;\n state.editorTestContent = files.test;\n state.editorActiveTab = 'main';\n\n const problemEl = document.getElementById('editor-problem');\n if (problemEl) {\n problemEl.innerHTML =\n '<h2>' + escapeHtml(exercise.title) + '</h2>' +\n '<div class=\"exercise-meta\">' +\n (exercise.difficulty ? '<span class=\"badge\">' + escapeHtml(exercise.difficulty) + '</span>' : '') +\n (exercise.est_minutes ? '<span class=\"badge\">' + exercise.est_minutes + ' min</span>' : '') +\n (exercise.status ? '<span class=\"badge ' + escapeHtml(exercise.status) + '\">' + escapeHtml(exercise.status) + '</span>' : '') +\n '</div>' +\n '<div class=\"description\">' + marked.parse(exercise.description || '') + '</div>';\n }\n\n const tabMain = document.getElementById('tab-main');\n const tabTest = document.getElementById('tab-test');\n if (tabMain) tabMain.textContent = files.mainFile || 'main.go';\n if (tabTest) tabTest.textContent = files.testFile || 'main_test.go';\n\n const outputBody = document.getElementById('editor-output-body');\n if (outputBody) outputBody.innerHTML = '<span class=\"text-muted\">Click \"Run Tests\" or press Ctrl+Enter</span>';\n\n showPage('exercise-editor');\n\n const cm = await loadCodeMirror();\n const container = document.getElementById('editor-container');\n if (!container) return;\n container.innerHTML = '';\n\n const runTestsKeymap = cm.keymap.of([{\n key: 'Mod-Enter',\n run: () => { runTestsFromEditor(); return true; },\n }]);\n\n state.editorView = new cm.EditorView({\n state: cm.EditorState.create({\n doc: state.editorMainContent,\n extensions: [cm.basicSetup, cm.go(), cm.oneDark, runTestsKeymap],\n }),\n parent: container,\n });\n}\n\nfunction switchEditorTab(tab) {\n if (!state.editorView || tab === state.editorActiveTab) return;\n\n const currentContent = state.editorView.state.doc.toString();\n if (state.editorActiveTab === 'main') {\n state.editorMainContent = currentContent;\n } else {\n state.editorTestContent = currentContent;\n }\n\n state.editorActiveTab = tab;\n\n const newContent = tab === 'main' ? state.editorMainContent : state.editorTestContent;\n state.editorView.dispatch({\n changes: {\n from: 0,\n to: state.editorView.state.doc.length,\n insert: newContent,\n },\n });\n\n document.querySelectorAll('.editor-tab').forEach(t => t.classList.remove('active'));\n const activeTab = document.getElementById(tab === 'main' ? 'tab-main' : 'tab-test');\n if (activeTab) activeTab.classList.add('active');\n}\n\nfunction closeExerciseEditor() {\n if (state.editorView) {\n state.editorView.destroy();\n state.editorView = null;\n }\n state.editorExercise = null;\n showPage('topic');\n switchTab('exercises');\n}\n\nasync function runTestsFromEditor() {\n if (!state.editorExercise) return;\n\n const exerciseId = state.editorExercise.id;\n const btn = document.getElementById('editor-run-btn');\n const outputBody = document.getElementById('editor-output-body');\n\n if (btn) { btn.textContent = 'Running...'; btn.disabled = true; }\n if (outputBody) outputBody.innerHTML = '<span class=\"text-muted\">Running tests...</span>';\n\n if (state.editorView) {\n const content = state.editorView.state.doc.toString();\n if (state.editorActiveTab === 'main') {\n state.editorMainContent = content;\n } else {\n state.editorTestContent = content;\n }\n }\n\n try {\n await fetch(`/api/exercises/${exerciseId}/files`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ main: state.editorMainContent, test: state.editorTestContent }),\n });\n\n const res = await fetch(`/api/exercises/${exerciseId}/run`, { method: 'POST' });\n const data = await res.json();\n\n if (data.error) {\n outputBody.innerHTML = '<div class=\"test-result-row fail\"><span class=\"test-icon\">✗</span><span class=\"test-name\">Error: ' + escapeHtml(data.error) + '</span></div>';\n } else if (Array.isArray(data)) {\n const passed = data.filter(r => r.passed).length;\n const total = data.length;\n const allPassed = passed === total;\n\n let html = '';\n for (const r of data) {\n const cls = r.passed ? 'pass' : 'fail';\n const icon = r.passed ? '✓' : '✗';\n html += '<div class=\"test-result-row ' + cls + '\">' +\n '<span class=\"test-icon\">' + icon + '</span>' +\n '<span class=\"test-name\">' + escapeHtml(r.test_name) + '</span>' +\n '</div>';\n if (r.output && !r.passed) {\n html += '<div class=\"test-result-output\">' + escapeHtml(r.output) + '</div>';\n }\n }\n\n html += '<div class=\"editor-progress-bar\">' +\n '<span>' + passed + '/' + total + ' passed</span>' +\n '<div class=\"editor-progress-fill\">' +\n '<div class=\"editor-progress-fill-inner ' + (allPassed ? 'green' : 'red') + '\" style=\"width:' + (total > 0 ? Math.round(passed / total * 100) : 0) + '%\"></div>' +\n '</div>' +\n '</div>';\n\n outputBody.innerHTML = html;\n state.editorExercise.status = allPassed ? 'passed' : 'failed';\n }\n } catch (err) {\n if (outputBody) outputBody.innerHTML = '<div class=\"test-result-row fail\"><span class=\"test-icon\">✗</span><span class=\"test-name\">Error: ' + escapeHtml(String(err)) + '</span></div>';\n } finally {\n if (btn) { btn.textContent = 'Run Tests'; btn.disabled = false; }\n }\n}\n\n// --- Navigation ---\nfunction showPage(page) {\n document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));\n\n const target = document.getElementById(`page-${page}`);\n if (target) target.classList.add('active');\n\n document.querySelectorAll('.nav-btn').forEach(btn => {\n btn.classList.toggle('active', btn.dataset.page === page);\n });\n\n if (page === 'home') renderHome();\n else if (page === 'topics') renderTopicsPage();\n else if (page === 'search') document.getElementById('search-input')?.focus();\n}\n\n// --- Search ---\nconst searchInput = document.getElementById('search-input');\nif (searchInput) {\n searchInput.addEventListener('input', (e) => {\n clearTimeout(state.searchTimeout);\n state.searchTimeout = setTimeout(() => doSearch(e.target.value, 'search-results'), 200);\n });\n}\n\nconst modalSearchInput = document.getElementById('modal-search-input');\nif (modalSearchInput) {\n modalSearchInput.addEventListener('input', (e) => {\n clearTimeout(state.searchTimeout);\n state.searchTimeout = setTimeout(() => doSearch(e.target.value, 'modal-search-results'), 200);\n });\n}\n\nasync function doSearch(query, resultsContainerId) {\n const container = document.getElementById(resultsContainerId);\n if (!container) return;\n\n if (!query || !query.trim()) {\n container.innerHTML = '';\n return;\n }\n\n try {\n const results = await api(`/api/search?q=${encodeURIComponent(query)}`);\n\n if (!results || results.length === 0) {\n container.innerHTML = '<div class=\"search-no-results\">No results found</div>';\n return;\n }\n\n container.innerHTML = results.map(r => `\n <div class=\"search-result-item\" onclick=\"closeSearchModal(); selectTopic(${r.topic_id})\">\n <div class=\"search-result-meta\">\n <span class=\"entry-kind ${escapeHtml(r.kind)}\">${escapeHtml(r.kind)}</span>\n </div>\n <div class=\"search-result-content\">${truncate(r.content, 150)}</div>\n </div>\n `).join('');\n } catch {\n container.innerHTML = '<div class=\"search-no-results\">Search failed</div>';\n }\n}\n\nfunction openSearchModal() {\n const modal = document.getElementById('search-modal');\n if (modal) {\n modal.classList.remove('hidden');\n const input = document.getElementById('modal-search-input');\n if (input) { input.value = ''; input.focus(); }\n const results = document.getElementById('modal-search-results');\n if (results) results.innerHTML = '';\n }\n}\n\nfunction closeSearchModal() {\n const modal = document.getElementById('search-modal');\n if (modal) modal.classList.add('hidden');\n}\n\n// --- Keyboard shortcuts ---\ndocument.addEventListener('keydown', (e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n openSearchModal();\n }\n if (e.key === 'Escape') {\n closeSearchModal();\n }\n});\n";
|
|
23730
23840
|
|
|
23731
|
-
var stylesCss = "/* ===== 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\n/* PDF resource cards */\n.resource-pdf {\n cursor: default;\n}\n\n.resource-pdf-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n cursor: pointer;\n padding: 0.75rem 1rem;\n}\n\n.resource-badge {\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n padding: 0.15rem 0.5rem;\n border-radius: 4px;\n background: #1f6feb33;\n color: #58a6ff;\n}\n\n.resource-chevron {\n margin-left: auto;\n font-size: 0.7rem;\n transition: transform 0.2s;\n color: #8b949e;\n}\n\n.resource-chevron.open {\n transform: rotate(180deg);\n}\n\n.resource-pdf-viewer {\n border-top: 1px solid #30363d;\n}\n\n.resource-pdf-viewer iframe {\n width: 100%;\n height: 70vh;\n border: none;\n background: #0d1117;\n}\n";
|
|
23841
|
+
var stylesCss = "/* ===== 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\n/* PDF resource cards */\n.resource-pdf {\n cursor: default;\n}\n\n.resource-pdf-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n cursor: pointer;\n padding: 0.75rem 1rem;\n}\n\n.resource-badge {\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n padding: 0.15rem 0.5rem;\n border-radius: 4px;\n background: #1f6feb33;\n color: #58a6ff;\n}\n\n.resource-chevron {\n margin-left: auto;\n font-size: 0.7rem;\n transition: transform 0.2s;\n color: #8b949e;\n}\n\n.resource-chevron.open {\n transform: rotate(180deg);\n}\n\n.resource-pdf-viewer {\n border-top: 1px solid #30363d;\n}\n\n.resource-pdf-viewer iframe {\n width: 100%;\n height: 70vh;\n border: none;\n background: #0d1117;\n}\n\n/* ── Exercise Editor ─────────────────────────────────────────────── */\n.exercise-editor {\n display: grid;\n grid-template-columns: 2fr 3fr;\n height: calc(100vh - 20px);\n gap: 1px;\n background: var(--border);\n}\n\n.exercise-editor-problem {\n background: var(--bg-primary);\n padding: 1.5rem;\n overflow-y: auto;\n}\n\n.exercise-editor-problem h2 {\n margin: 0 0 0.5rem 0;\n font-size: 1.25rem;\n}\n\n.exercise-editor-problem .exercise-meta {\n display: flex;\n gap: 0.75rem;\n margin-bottom: 1rem;\n flex-wrap: wrap;\n}\n\n.exercise-editor-problem .description {\n line-height: 1.7;\n color: var(--text-secondary);\n}\n\n.exercise-editor-problem .description pre {\n background: var(--bg-secondary);\n padding: 0.75rem;\n border-radius: 6px;\n overflow-x: auto;\n}\n\n.exercise-editor-right {\n display: flex;\n flex-direction: column;\n background: var(--bg-primary);\n min-height: 0;\n}\n\n.exercise-editor-code {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.editor-tabs {\n display: flex;\n gap: 0;\n background: var(--bg-secondary);\n border-bottom: 1px solid var(--border);\n padding: 0 0.5rem;\n align-items: center;\n}\n\n.editor-tab {\n padding: 0.5rem 1rem;\n background: none;\n border: none;\n color: var(--text-secondary);\n cursor: pointer;\n font-size: 0.8rem;\n font-family: monospace;\n border-bottom: 2px solid transparent;\n}\n\n.editor-tab:hover {\n color: var(--text);\n}\n\n.editor-tab.active {\n color: var(--accent);\n border-bottom-color: var(--accent);\n}\n\n.editor-back-btn {\n margin-left: auto;\n padding: 0.3rem 0.75rem;\n background: none;\n border: 1px solid var(--border);\n color: var(--text-secondary);\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.75rem;\n}\n\n.editor-back-btn:hover {\n color: var(--text);\n border-color: var(--text-secondary);\n}\n\n#editor-container {\n flex: 1;\n overflow: hidden;\n}\n\n#editor-container .cm-editor {\n height: 100%;\n}\n\n#editor-container .cm-scroller {\n overflow: auto;\n}\n\n.exercise-editor-output {\n height: 200px;\n border-top: 1px solid var(--border);\n display: flex;\n flex-direction: column;\n}\n\n.editor-output-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n background: var(--bg-secondary);\n font-size: 0.8rem;\n font-weight: 600;\n color: var(--text-secondary);\n}\n\n.editor-run-btn {\n padding: 0.35rem 1rem;\n background: #238636;\n color: #fff;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.8rem;\n font-weight: 600;\n}\n\n.editor-run-btn:hover {\n background: #2ea043;\n}\n\n.editor-run-btn:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.editor-output-body {\n flex: 1;\n overflow-y: auto;\n padding: 0.75rem 1rem;\n font-family: monospace;\n font-size: 0.8rem;\n}\n\n.test-result-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.3rem 0;\n}\n\n.test-result-row .test-icon {\n font-size: 0.9rem;\n}\n\n.test-result-row.pass .test-icon { color: var(--green); }\n.test-result-row.fail .test-icon { color: #f85149; }\n.test-result-row.pass .test-name { color: var(--text-secondary); }\n.test-result-row.fail .test-name { color: var(--text); }\n\n.test-result-output {\n margin: 0.25rem 0 0.5rem 1.5rem;\n padding: 0.5rem;\n background: var(--bg-secondary);\n border-radius: 4px;\n font-size: 0.75rem;\n color: #f85149;\n white-space: pre-wrap;\n word-break: break-all;\n}\n\n.editor-progress-bar {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.5rem 1rem;\n border-top: 1px solid var(--border);\n font-size: 0.8rem;\n color: var(--text-secondary);\n}\n\n.editor-progress-fill {\n flex: 1;\n height: 4px;\n background: var(--bg-tertiary);\n border-radius: 2px;\n overflow: hidden;\n}\n\n.editor-progress-fill-inner {\n height: 100%;\n border-radius: 2px;\n transition: width 0.3s;\n}\n\n.editor-progress-fill-inner.green { background: var(--green); }\n.editor-progress-fill-inner.red { background: #f85149; }\n\n@media (max-width: 768px) {\n .exercise-editor {\n grid-template-columns: 1fr;\n grid-template-rows: auto 1fr;\n height: auto;\n }\n .exercise-editor-problem {\n max-height: 40vh;\n }\n}\n";
|
|
23732
23842
|
|
|
23733
23843
|
const STATIC_FILES = {
|
|
23734
23844
|
'/': { content: indexHtml, contentType: 'text/html; charset=utf-8' },
|
|
@@ -23853,6 +23963,16 @@ class DashboardServer {
|
|
|
23853
23963
|
handleTopic(this.curriculumSvc, this.qaSvc, this.resourceSvc)(req, res);
|
|
23854
23964
|
return;
|
|
23855
23965
|
}
|
|
23966
|
+
// GET /api/exercises/:id/files
|
|
23967
|
+
if (method === 'GET' && /^\/api\/exercises\/\d+\/files$/.test(path)) {
|
|
23968
|
+
handleExerciseFiles(this.exerciseSvc)(req, res);
|
|
23969
|
+
return;
|
|
23970
|
+
}
|
|
23971
|
+
// POST /api/exercises/:id/files
|
|
23972
|
+
if (method === 'POST' && /^\/api\/exercises\/\d+\/files$/.test(path)) {
|
|
23973
|
+
handleSaveExerciseFiles(this.exerciseSvc)(req, res);
|
|
23974
|
+
return;
|
|
23975
|
+
}
|
|
23856
23976
|
// POST /api/exercises/:id/run
|
|
23857
23977
|
if (method === 'POST' && /^\/api\/exercises\/\d+\/run$/.test(path)) {
|
|
23858
23978
|
handleRunTests(this.exerciseSvc)(req, res);
|
|
@@ -23898,6 +24018,11 @@ const curriculumSvc = new CurriculumService(db);
|
|
|
23898
24018
|
const qaSvc = new QAService(db);
|
|
23899
24019
|
const vizSvc = new VizService(db);
|
|
23900
24020
|
const exerciseSvc = new ExerciseService(db, fileStore);
|
|
24021
|
+
// Migrate legacy .txt exercise files to correct extensions
|
|
24022
|
+
const migrated = exerciseSvc.migrateFileExtensions();
|
|
24023
|
+
if (migrated > 0) {
|
|
24024
|
+
console.error(`Migrated ${migrated} exercise file(s) to correct extensions`);
|
|
24025
|
+
}
|
|
23901
24026
|
const resourceSvc = new ResourceService(db);
|
|
23902
24027
|
const port = Number(db.getSetting('dashboard_port') ?? '19282');
|
|
23903
24028
|
const dashboard = new DashboardServer(curriculumSvc, qaSvc, vizSvc, exerciseSvc, resourceSvc, port);
|
|
@@ -16,4 +16,6 @@ export declare function handleTopicExercises(exerciseSvc: ExerciseService): (req
|
|
|
16
16
|
export declare function handleRunTests(exerciseSvc: ExerciseService): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
17
17
|
export declare function handleSubmitQuiz(exerciseSvc: ExerciseService): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
18
18
|
export declare function handleSearch(qaSvc: QAService): (req: IncomingMessage, res: ServerResponse) => void;
|
|
19
|
+
export declare function handleExerciseFiles(exerciseSvc: ExerciseService): (req: IncomingMessage, res: ServerResponse) => void;
|
|
20
|
+
export declare function handleSaveExerciseFiles(exerciseSvc: ExerciseService): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
19
21
|
export declare function handleResourceFile(resourceSvc: ResourceService): (req: IncomingMessage, res: ServerResponse) => void;
|
|
@@ -161,6 +161,43 @@ export function handleSearch(qaSvc) {
|
|
|
161
161
|
}
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
|
+
export function handleExerciseFiles(exerciseSvc) {
|
|
165
|
+
return (req, res) => {
|
|
166
|
+
const id = extractId(req.url ?? '', '/api/exercises/');
|
|
167
|
+
if (id === null) {
|
|
168
|
+
writeError(res, 400, 'Invalid exercise ID');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const files = exerciseSvc.getExerciseFiles(id);
|
|
172
|
+
if (!files) {
|
|
173
|
+
writeError(res, 404, 'Exercise not found');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
writeJSON(res, files);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function handleSaveExerciseFiles(exerciseSvc) {
|
|
180
|
+
return async (req, res) => {
|
|
181
|
+
const id = extractId(req.url ?? '', '/api/exercises/');
|
|
182
|
+
if (id === null) {
|
|
183
|
+
writeError(res, 400, 'Invalid exercise ID');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const body = (await parseBody(req));
|
|
188
|
+
if (typeof body?.main !== 'string' || typeof body?.test !== 'string') {
|
|
189
|
+
writeError(res, 400, 'Request body must have "main" and "test" strings');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
exerciseSvc.saveExerciseFiles(id, body.main, body.test);
|
|
193
|
+
writeJSON(res, { ok: true });
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
197
|
+
writeError(res, 500, msg);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
164
201
|
export function handleResourceFile(resourceSvc) {
|
|
165
202
|
return (req, res) => {
|
|
166
203
|
const id = extractId(req.url ?? '', '/api/resources/');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/dashboard/api.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/dashboard/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
import { handleSubjects, handlePhases, handleTopic, handleTopicViz, handleTopicExercises, handleTopicResources, handleResourceFile, handleRunTests, handleSubmitQuiz, handleSearch, writeJSON, } from './api.js';
|
|
2
|
+
import { handleSubjects, handlePhases, handleTopic, handleTopicViz, handleTopicExercises, handleTopicResources, handleResourceFile, handleExerciseFiles, handleSaveExerciseFiles, 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
|
|
@@ -131,6 +131,16 @@ export class DashboardServer {
|
|
|
131
131
|
handleTopic(this.curriculumSvc, this.qaSvc, this.resourceSvc)(req, res);
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
|
+
// GET /api/exercises/:id/files
|
|
135
|
+
if (method === 'GET' && /^\/api\/exercises\/\d+\/files$/.test(path)) {
|
|
136
|
+
handleExerciseFiles(this.exerciseSvc)(req, res);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// POST /api/exercises/:id/files
|
|
140
|
+
if (method === 'POST' && /^\/api\/exercises\/\d+\/files$/.test(path)) {
|
|
141
|
+
handleSaveExerciseFiles(this.exerciseSvc)(req, res);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
134
144
|
// POST /api/exercises/:id/run
|
|
135
145
|
if (method === 'POST' && /^\/api\/exercises\/\d+\/run$/.test(path)) {
|
|
136
146
|
handleRunTests(this.exerciseSvc)(req, res);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"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"}
|
package/server/dist/index.js
CHANGED
|
@@ -21,6 +21,11 @@ const curriculumSvc = new CurriculumService(db);
|
|
|
21
21
|
const qaSvc = new QAService(db);
|
|
22
22
|
const vizSvc = new VizService(db);
|
|
23
23
|
const exerciseSvc = new ExerciseService(db, fileStore);
|
|
24
|
+
// Migrate legacy .txt exercise files to correct extensions
|
|
25
|
+
const migrated = exerciseSvc.migrateFileExtensions();
|
|
26
|
+
if (migrated > 0) {
|
|
27
|
+
console.error(`Migrated ${migrated} exercise file(s) to correct extensions`);
|
|
28
|
+
}
|
|
24
29
|
const resourceSvc = new ResourceService(db);
|
|
25
30
|
const port = Number(db.getSetting('dashboard_port') ?? '19282');
|
|
26
31
|
const dashboard = new DashboardServer(curriculumSvc, qaSvc, vizSvc, exerciseSvc, resourceSvc, port);
|
package/server/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,13 @@ interface QuizSubmitResult {
|
|
|
22
22
|
output: string;
|
|
23
23
|
}>;
|
|
24
24
|
}
|
|
25
|
+
export interface ExerciseFiles {
|
|
26
|
+
main: string;
|
|
27
|
+
test: string;
|
|
28
|
+
language: string;
|
|
29
|
+
mainFile: string;
|
|
30
|
+
testFile: string;
|
|
31
|
+
}
|
|
25
32
|
export declare class ExerciseService {
|
|
26
33
|
private db;
|
|
27
34
|
private fileStore;
|
|
@@ -30,6 +37,9 @@ export declare class ExerciseService {
|
|
|
30
37
|
runTests(exerciseId: number): Promise<ExerciseResult[]>;
|
|
31
38
|
submitQuiz(exerciseId: number, answers: (number | boolean | string)[]): QuizSubmitResult;
|
|
32
39
|
listForTopic(topicId: number): Exercise[];
|
|
40
|
+
getExerciseFiles(exerciseId: number): ExerciseFiles | undefined;
|
|
41
|
+
saveExerciseFiles(exerciseId: number, main: string, test: string): void;
|
|
42
|
+
migrateFileExtensions(): number;
|
|
33
43
|
private getSubjectForTopic;
|
|
34
44
|
}
|
|
35
45
|
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFileSync, existsSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { promisify } from 'node:util';
|
|
3
5
|
const execFileAsync = promisify(execFile);
|
|
4
6
|
function slugify(name) {
|
|
@@ -43,7 +45,7 @@ export class ExerciseService {
|
|
|
43
45
|
const subject = this.getSubjectForTopic(topicId);
|
|
44
46
|
if (subject) {
|
|
45
47
|
const exerciseSlug = slugify(title);
|
|
46
|
-
const ext = extensionForLanguage(subject.language);
|
|
48
|
+
const ext = extensionForLanguage(subject.language.toLowerCase());
|
|
47
49
|
const files = {};
|
|
48
50
|
if (starter_code) {
|
|
49
51
|
files[`main${ext}`] = starter_code;
|
|
@@ -80,7 +82,7 @@ export class ExerciseService {
|
|
|
80
82
|
javascript: { command: 'npx', args: ['vitest', 'run'] },
|
|
81
83
|
typescript: { command: 'npx', args: ['vitest', 'run'] },
|
|
82
84
|
};
|
|
83
|
-
const config = commandMap[subject.language];
|
|
85
|
+
const config = commandMap[subject.language.toLowerCase()];
|
|
84
86
|
if (!config)
|
|
85
87
|
throw new Error(`Unsupported language: ${subject.language}`);
|
|
86
88
|
let stdout = '';
|
|
@@ -203,6 +205,79 @@ export class ExerciseService {
|
|
|
203
205
|
.prepare('SELECT * FROM exercises WHERE topic_id = ? ORDER BY created_at ASC, id ASC')
|
|
204
206
|
.all(topicId);
|
|
205
207
|
}
|
|
208
|
+
getExerciseFiles(exerciseId) {
|
|
209
|
+
const exercise = this.db.raw
|
|
210
|
+
.prepare('SELECT * FROM exercises WHERE id = ?')
|
|
211
|
+
.get(exerciseId);
|
|
212
|
+
if (!exercise)
|
|
213
|
+
return undefined;
|
|
214
|
+
const subject = this.getSubjectForTopic(exercise.topic_id);
|
|
215
|
+
const lang = subject?.language.toLowerCase() ?? '';
|
|
216
|
+
const ext = extensionForLanguage(lang);
|
|
217
|
+
const mainFile = `main${ext}`;
|
|
218
|
+
const testFile = `main_test${ext}`;
|
|
219
|
+
let main = '';
|
|
220
|
+
let test = '';
|
|
221
|
+
if (exercise.file_path) {
|
|
222
|
+
try {
|
|
223
|
+
main = readFileSync(join(exercise.file_path, mainFile), 'utf-8');
|
|
224
|
+
}
|
|
225
|
+
catch { }
|
|
226
|
+
try {
|
|
227
|
+
test = readFileSync(join(exercise.file_path, testFile), 'utf-8');
|
|
228
|
+
}
|
|
229
|
+
catch { }
|
|
230
|
+
}
|
|
231
|
+
return { main, test, language: lang, mainFile, testFile };
|
|
232
|
+
}
|
|
233
|
+
saveExerciseFiles(exerciseId, main, test) {
|
|
234
|
+
const exercise = this.db.raw
|
|
235
|
+
.prepare('SELECT * FROM exercises WHERE id = ?')
|
|
236
|
+
.get(exerciseId);
|
|
237
|
+
if (!exercise)
|
|
238
|
+
throw new Error(`Exercise ${exerciseId} not found`);
|
|
239
|
+
const subject = this.getSubjectForTopic(exercise.topic_id);
|
|
240
|
+
if (!subject)
|
|
241
|
+
throw new Error(`No subject found for exercise ${exerciseId}`);
|
|
242
|
+
const lang = subject.language.toLowerCase();
|
|
243
|
+
const ext = extensionForLanguage(lang);
|
|
244
|
+
let filePath = exercise.file_path;
|
|
245
|
+
if (!filePath) {
|
|
246
|
+
const exerciseSlug = slugify(exercise.title);
|
|
247
|
+
filePath = this.fileStore.writeExerciseFiles(subject.slug, exerciseSlug, {});
|
|
248
|
+
this.db.raw.prepare('UPDATE exercises SET file_path = ? WHERE id = ?').run(filePath, exerciseId);
|
|
249
|
+
}
|
|
250
|
+
writeFileSync(join(filePath, `main${ext}`), main, 'utf-8');
|
|
251
|
+
writeFileSync(join(filePath, `main_test${ext}`), test, 'utf-8');
|
|
252
|
+
}
|
|
253
|
+
migrateFileExtensions() {
|
|
254
|
+
const exercises = this.db.raw
|
|
255
|
+
.prepare("SELECT * FROM exercises WHERE file_path != '' AND type IN ('coding', 'project')")
|
|
256
|
+
.all();
|
|
257
|
+
let migrated = 0;
|
|
258
|
+
for (const exercise of exercises) {
|
|
259
|
+
const subject = this.getSubjectForTopic(exercise.topic_id);
|
|
260
|
+
if (!subject)
|
|
261
|
+
continue;
|
|
262
|
+
const ext = extensionForLanguage(subject.language.toLowerCase());
|
|
263
|
+
if (ext === '.txt')
|
|
264
|
+
continue;
|
|
265
|
+
const dir = exercise.file_path;
|
|
266
|
+
const mainTxt = join(dir, 'main.txt');
|
|
267
|
+
const testTxt = join(dir, 'main_test.txt');
|
|
268
|
+
const mainTarget = join(dir, `main${ext}`);
|
|
269
|
+
const testTarget = join(dir, `main_test${ext}`);
|
|
270
|
+
if (existsSync(mainTxt) && !existsSync(mainTarget)) {
|
|
271
|
+
renameSync(mainTxt, mainTarget);
|
|
272
|
+
migrated++;
|
|
273
|
+
}
|
|
274
|
+
if (existsSync(testTxt) && !existsSync(testTarget)) {
|
|
275
|
+
renameSync(testTxt, testTarget);
|
|
276
|
+
migrated++;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return migrated;
|
|
280
|
+
}
|
|
206
281
|
getSubjectForTopic(topicId) {
|
|
207
282
|
return this.db.raw
|
|
208
283
|
.prepare(`SELECT s.* FROM subjects s
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exercises.js","sourceRoot":"","sources":["../../src/services/exercises.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"exercises.js","sourceRoot":"","sources":["../../src/services/exercises.ts"],"names":[],"mappings":"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"}
|