@shaykec/bridge 0.1.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/README.md +65 -0
- package/package.json +26 -0
- package/src/protocol.js +303 -0
- package/src/protocol.test.js +373 -0
- package/src/router.js +149 -0
- package/src/router.test.js +329 -0
- package/src/server.js +497 -0
- package/src/templates.js +155 -0
- package/src/templates.test.js +256 -0
- package/templates/celebrate.html +259 -0
- package/templates/code-playground.html +294 -0
- package/templates/dashboard.html +337 -0
- package/templates/diagram-architecture.html +449 -0
- package/templates/diagram-flow.html +382 -0
- package/templates/diagram-mermaid.html +220 -0
- package/templates/quiz-drag-order.html +375 -0
- package/templates/quiz-fill-blank.html +468 -0
- package/templates/quiz-matching.html +501 -0
- package/templates/quiz-timed-choice.html +361 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ClaudeTeach — Code Playground</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
background: #0d1117;
|
|
12
|
+
color: #c9d1d9;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
padding: 1.5rem;
|
|
17
|
+
}
|
|
18
|
+
.header {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
margin-bottom: 1rem;
|
|
23
|
+
}
|
|
24
|
+
h2 {
|
|
25
|
+
color: #58a6ff;
|
|
26
|
+
font-size: 1.1rem;
|
|
27
|
+
}
|
|
28
|
+
.language-badge {
|
|
29
|
+
background: #1c2333;
|
|
30
|
+
border: 1px solid #30363d;
|
|
31
|
+
color: #8b949e;
|
|
32
|
+
padding: 0.3rem 0.7rem;
|
|
33
|
+
border-radius: 12px;
|
|
34
|
+
font-size: 0.8rem;
|
|
35
|
+
text-transform: uppercase;
|
|
36
|
+
}
|
|
37
|
+
.instructions {
|
|
38
|
+
background: #161b22;
|
|
39
|
+
border: 1px solid #30363d;
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
padding: 0.8rem 1rem;
|
|
42
|
+
margin-bottom: 1rem;
|
|
43
|
+
font-size: 0.9rem;
|
|
44
|
+
line-height: 1.5;
|
|
45
|
+
color: #8b949e;
|
|
46
|
+
}
|
|
47
|
+
.editor-container {
|
|
48
|
+
flex: 1;
|
|
49
|
+
min-height: 250px;
|
|
50
|
+
border: 1px solid #30363d;
|
|
51
|
+
border-radius: 8px;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
margin-bottom: 0.75rem;
|
|
54
|
+
position: relative;
|
|
55
|
+
}
|
|
56
|
+
/* Fallback textarea editor (Monaco loaded lazily) */
|
|
57
|
+
.code-editor {
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 100%;
|
|
60
|
+
min-height: 250px;
|
|
61
|
+
background: #0d1117;
|
|
62
|
+
color: #c9d1d9;
|
|
63
|
+
border: none;
|
|
64
|
+
padding: 1rem;
|
|
65
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
66
|
+
font-size: 14px;
|
|
67
|
+
line-height: 1.6;
|
|
68
|
+
resize: none;
|
|
69
|
+
outline: none;
|
|
70
|
+
tab-size: 2;
|
|
71
|
+
}
|
|
72
|
+
.code-editor:focus {
|
|
73
|
+
outline: none;
|
|
74
|
+
}
|
|
75
|
+
.toolbar {
|
|
76
|
+
display: flex;
|
|
77
|
+
gap: 0.5rem;
|
|
78
|
+
margin-bottom: 0.75rem;
|
|
79
|
+
}
|
|
80
|
+
.btn {
|
|
81
|
+
padding: 0.5rem 1rem;
|
|
82
|
+
border: none;
|
|
83
|
+
border-radius: 6px;
|
|
84
|
+
font-size: 0.9rem;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
font-weight: 500;
|
|
87
|
+
transition: background 0.15s;
|
|
88
|
+
}
|
|
89
|
+
.btn-run {
|
|
90
|
+
background: #238636;
|
|
91
|
+
color: #fff;
|
|
92
|
+
}
|
|
93
|
+
.btn-run:hover { background: #2ea043; }
|
|
94
|
+
.btn-reset {
|
|
95
|
+
background: #21262d;
|
|
96
|
+
color: #c9d1d9;
|
|
97
|
+
border: 1px solid #30363d;
|
|
98
|
+
}
|
|
99
|
+
.btn-reset:hover { background: #30363d; }
|
|
100
|
+
.btn-hint {
|
|
101
|
+
background: #1c2333;
|
|
102
|
+
color: #58a6ff;
|
|
103
|
+
border: 1px solid #30363d;
|
|
104
|
+
margin-left: auto;
|
|
105
|
+
}
|
|
106
|
+
.btn-hint:hover { background: #283040; }
|
|
107
|
+
.output-container {
|
|
108
|
+
background: #161b22;
|
|
109
|
+
border: 1px solid #30363d;
|
|
110
|
+
border-radius: 8px;
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
}
|
|
113
|
+
.output-header {
|
|
114
|
+
background: #21262d;
|
|
115
|
+
padding: 0.5rem 1rem;
|
|
116
|
+
font-size: 0.8rem;
|
|
117
|
+
color: #8b949e;
|
|
118
|
+
text-transform: uppercase;
|
|
119
|
+
letter-spacing: 0.05em;
|
|
120
|
+
border-bottom: 1px solid #30363d;
|
|
121
|
+
}
|
|
122
|
+
.output {
|
|
123
|
+
padding: 1rem;
|
|
124
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
125
|
+
font-size: 13px;
|
|
126
|
+
line-height: 1.5;
|
|
127
|
+
white-space: pre-wrap;
|
|
128
|
+
min-height: 80px;
|
|
129
|
+
max-height: 200px;
|
|
130
|
+
overflow-y: auto;
|
|
131
|
+
color: #8b949e;
|
|
132
|
+
}
|
|
133
|
+
.output.has-output { color: #c9d1d9; }
|
|
134
|
+
.output.has-error { color: #f85149; }
|
|
135
|
+
.output.has-success { color: #3fb950; }
|
|
136
|
+
</style>
|
|
137
|
+
</head>
|
|
138
|
+
<body>
|
|
139
|
+
<div class="header">
|
|
140
|
+
<h2>Code Playground</h2>
|
|
141
|
+
<span class="language-badge" id="langBadge">javascript</span>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="instructions" id="instructions">
|
|
144
|
+
Write your code below and click Run to execute.
|
|
145
|
+
</div>
|
|
146
|
+
<div class="editor-container">
|
|
147
|
+
<textarea class="code-editor" id="editor" spellcheck="false"
|
|
148
|
+
placeholder="Type your code here..."></textarea>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="toolbar">
|
|
151
|
+
<button class="btn btn-run" onclick="runCode()">Run</button>
|
|
152
|
+
<button class="btn btn-reset" onclick="resetCode()">Reset</button>
|
|
153
|
+
<button class="btn btn-hint" onclick="showHint()">Hint</button>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="output-container">
|
|
156
|
+
<div class="output-header">Output</div>
|
|
157
|
+
<div class="output" id="output">Click "Run" to execute your code.</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<script>
|
|
161
|
+
/* --- Bridge connection (WebSocket + REST fallback) --- */
|
|
162
|
+
var wsBridge = null;
|
|
163
|
+
(function connectBridge() {
|
|
164
|
+
try {
|
|
165
|
+
wsBridge = new WebSocket('ws://' + location.host + '/ws');
|
|
166
|
+
wsBridge.onopen = function() {
|
|
167
|
+
wsBridge.send(JSON.stringify({
|
|
168
|
+
v: 1, type: 'sys:connect',
|
|
169
|
+
payload: { clientType: 'template' },
|
|
170
|
+
source: 'template', timestamp: Date.now()
|
|
171
|
+
}));
|
|
172
|
+
};
|
|
173
|
+
wsBridge.onerror = function() { wsBridge = null; };
|
|
174
|
+
wsBridge.onclose = function() { wsBridge = null; };
|
|
175
|
+
} catch(e) { wsBridge = null; }
|
|
176
|
+
})();
|
|
177
|
+
|
|
178
|
+
let playgroundData = {
|
|
179
|
+
language: 'javascript',
|
|
180
|
+
starter: '',
|
|
181
|
+
instructions: 'Write your code below and click Run to execute.',
|
|
182
|
+
hint: null,
|
|
183
|
+
validation: null,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function init(data) {
|
|
187
|
+
if (data) playgroundData = { ...playgroundData, ...data };
|
|
188
|
+
|
|
189
|
+
document.getElementById('langBadge').textContent = playgroundData.language;
|
|
190
|
+
document.getElementById('instructions').textContent = playgroundData.instructions;
|
|
191
|
+
|
|
192
|
+
if (playgroundData.starter) {
|
|
193
|
+
document.getElementById('editor').value = playgroundData.starter;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Handle Tab key for indentation
|
|
197
|
+
document.getElementById('editor').addEventListener('keydown', (e) => {
|
|
198
|
+
if (e.key === 'Tab') {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
const textarea = e.target;
|
|
201
|
+
const start = textarea.selectionStart;
|
|
202
|
+
const end = textarea.selectionEnd;
|
|
203
|
+
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
|
|
204
|
+
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function runCode() {
|
|
210
|
+
const code = document.getElementById('editor').value;
|
|
211
|
+
const outputEl = document.getElementById('output');
|
|
212
|
+
const lang = playgroundData.language;
|
|
213
|
+
|
|
214
|
+
if (lang === 'javascript') {
|
|
215
|
+
// Sandboxed JS execution
|
|
216
|
+
try {
|
|
217
|
+
const logs = [];
|
|
218
|
+
const mockConsole = {
|
|
219
|
+
log: (...args) => logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ')),
|
|
220
|
+
error: (...args) => logs.push('ERROR: ' + args.join(' ')),
|
|
221
|
+
warn: (...args) => logs.push('WARN: ' + args.join(' ')),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const fn = new Function('console', code);
|
|
225
|
+
fn(mockConsole);
|
|
226
|
+
|
|
227
|
+
const output = logs.join('\n') || '(no output)';
|
|
228
|
+
outputEl.textContent = output;
|
|
229
|
+
outputEl.className = 'output has-output';
|
|
230
|
+
|
|
231
|
+
sendResult(code, output, 0);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
outputEl.textContent = `Error: ${err.message}`;
|
|
234
|
+
outputEl.className = 'output has-error';
|
|
235
|
+
sendResult(code, err.message, 1);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
// For non-JS languages, we can't execute locally
|
|
239
|
+
outputEl.textContent = `[${lang}] Code submitted for execution.\nLocal execution is only available for JavaScript.\nYour code has been sent to the learning session.`;
|
|
240
|
+
outputEl.className = 'output has-output';
|
|
241
|
+
sendResult(code, '(sent to server)', 0);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resetCode() {
|
|
246
|
+
document.getElementById('editor').value = playgroundData.starter || '';
|
|
247
|
+
document.getElementById('output').textContent = 'Click "Run" to execute your code.';
|
|
248
|
+
document.getElementById('output').className = 'output';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function showHint() {
|
|
252
|
+
if (playgroundData.hint) {
|
|
253
|
+
const outputEl = document.getElementById('output');
|
|
254
|
+
outputEl.textContent = 'Hint: ' + playgroundData.hint;
|
|
255
|
+
outputEl.className = 'output has-output';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function sendResult(code, output, exitCode) {
|
|
260
|
+
var msg = {
|
|
261
|
+
v: 1, type: 'event:code-run',
|
|
262
|
+
payload: {
|
|
263
|
+
code,
|
|
264
|
+
output,
|
|
265
|
+
exitCode,
|
|
266
|
+
language: playgroundData.language,
|
|
267
|
+
},
|
|
268
|
+
source: 'template', timestamp: Date.now()
|
|
269
|
+
};
|
|
270
|
+
if (wsBridge && wsBridge.readyState === 1) { wsBridge.send(JSON.stringify(msg)); }
|
|
271
|
+
try {
|
|
272
|
+
fetch('/api/event', {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify(msg)
|
|
276
|
+
}).catch(function(){});
|
|
277
|
+
} catch(e) {}
|
|
278
|
+
if (window.parent !== window) { window.parent.postMessage(msg, '*'); }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
window.addEventListener('message', (e) => {
|
|
282
|
+
if (e.data && e.data.type === 'playground-data') {
|
|
283
|
+
init(e.data.payload);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (window.__PLAYGROUND_DATA__) {
|
|
288
|
+
init(window.__PLAYGROUND_DATA__);
|
|
289
|
+
} else {
|
|
290
|
+
init();
|
|
291
|
+
}
|
|
292
|
+
</script>
|
|
293
|
+
</body>
|
|
294
|
+
</html>
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ClaudeTeach — Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
background: #0d1117;
|
|
12
|
+
color: #c9d1d9;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
padding: 2rem;
|
|
15
|
+
}
|
|
16
|
+
.dashboard {
|
|
17
|
+
max-width: 700px;
|
|
18
|
+
margin: 0 auto;
|
|
19
|
+
}
|
|
20
|
+
.dashboard-header {
|
|
21
|
+
text-align: center;
|
|
22
|
+
margin-bottom: 2rem;
|
|
23
|
+
}
|
|
24
|
+
.dashboard-header h1 {
|
|
25
|
+
color: #58a6ff;
|
|
26
|
+
font-size: 1.5rem;
|
|
27
|
+
margin-bottom: 0.5rem;
|
|
28
|
+
}
|
|
29
|
+
.belt-display {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
gap: 1rem;
|
|
34
|
+
margin-bottom: 1.5rem;
|
|
35
|
+
}
|
|
36
|
+
.belt-icon {
|
|
37
|
+
font-size: 3rem;
|
|
38
|
+
}
|
|
39
|
+
.belt-info h2 {
|
|
40
|
+
font-size: 1.2rem;
|
|
41
|
+
color: #c9d1d9;
|
|
42
|
+
}
|
|
43
|
+
.belt-info .xp {
|
|
44
|
+
color: #8b949e;
|
|
45
|
+
font-size: 0.9rem;
|
|
46
|
+
}
|
|
47
|
+
.progress-bar-container {
|
|
48
|
+
background: #21262d;
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
height: 12px;
|
|
51
|
+
margin: 0.5rem 0;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
}
|
|
54
|
+
.progress-bar-fill {
|
|
55
|
+
height: 100%;
|
|
56
|
+
border-radius: 8px;
|
|
57
|
+
background: linear-gradient(90deg, #238636, #3fb950);
|
|
58
|
+
transition: width 0.6s ease;
|
|
59
|
+
}
|
|
60
|
+
.progress-label {
|
|
61
|
+
display: flex;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
font-size: 0.8rem;
|
|
64
|
+
color: #8b949e;
|
|
65
|
+
margin-bottom: 1.5rem;
|
|
66
|
+
}
|
|
67
|
+
.stats-grid {
|
|
68
|
+
display: grid;
|
|
69
|
+
grid-template-columns: repeat(3, 1fr);
|
|
70
|
+
gap: 1rem;
|
|
71
|
+
margin-bottom: 2rem;
|
|
72
|
+
}
|
|
73
|
+
.stat-card {
|
|
74
|
+
background: #161b22;
|
|
75
|
+
border: 1px solid #30363d;
|
|
76
|
+
border-radius: 10px;
|
|
77
|
+
padding: 1rem;
|
|
78
|
+
text-align: center;
|
|
79
|
+
}
|
|
80
|
+
.stat-value {
|
|
81
|
+
font-size: 1.8rem;
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
color: #58a6ff;
|
|
84
|
+
}
|
|
85
|
+
.stat-label {
|
|
86
|
+
font-size: 0.8rem;
|
|
87
|
+
color: #8b949e;
|
|
88
|
+
margin-top: 0.3rem;
|
|
89
|
+
}
|
|
90
|
+
.section-title {
|
|
91
|
+
font-size: 1rem;
|
|
92
|
+
color: #c9d1d9;
|
|
93
|
+
margin-bottom: 0.75rem;
|
|
94
|
+
padding-bottom: 0.5rem;
|
|
95
|
+
border-bottom: 1px solid #21262d;
|
|
96
|
+
}
|
|
97
|
+
.module-list {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 0.5rem;
|
|
101
|
+
margin-bottom: 2rem;
|
|
102
|
+
}
|
|
103
|
+
.module-card {
|
|
104
|
+
background: #161b22;
|
|
105
|
+
border: 1px solid #30363d;
|
|
106
|
+
border-radius: 8px;
|
|
107
|
+
padding: 0.8rem 1rem;
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: 0.75rem;
|
|
111
|
+
}
|
|
112
|
+
.module-status {
|
|
113
|
+
width: 32px;
|
|
114
|
+
height: 32px;
|
|
115
|
+
border-radius: 50%;
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
font-size: 0.9rem;
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
}
|
|
122
|
+
.module-status.completed {
|
|
123
|
+
background: rgba(35, 134, 54, 0.2);
|
|
124
|
+
color: #3fb950;
|
|
125
|
+
}
|
|
126
|
+
.module-status.in-progress {
|
|
127
|
+
background: rgba(210, 153, 34, 0.2);
|
|
128
|
+
color: #d29922;
|
|
129
|
+
}
|
|
130
|
+
.module-status.locked {
|
|
131
|
+
background: #21262d;
|
|
132
|
+
color: #484f58;
|
|
133
|
+
}
|
|
134
|
+
.module-info { flex: 1; }
|
|
135
|
+
.module-name { font-size: 0.95rem; }
|
|
136
|
+
.module-xp {
|
|
137
|
+
font-size: 0.8rem;
|
|
138
|
+
color: #8b949e;
|
|
139
|
+
}
|
|
140
|
+
.recommendation {
|
|
141
|
+
background: #1c2333;
|
|
142
|
+
border: 1px solid #30363d;
|
|
143
|
+
border-radius: 10px;
|
|
144
|
+
padding: 1.2rem;
|
|
145
|
+
text-align: center;
|
|
146
|
+
}
|
|
147
|
+
.recommendation h3 {
|
|
148
|
+
color: #58a6ff;
|
|
149
|
+
font-size: 0.95rem;
|
|
150
|
+
margin-bottom: 0.5rem;
|
|
151
|
+
}
|
|
152
|
+
.recommendation p {
|
|
153
|
+
color: #8b949e;
|
|
154
|
+
font-size: 0.9rem;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
157
|
+
</head>
|
|
158
|
+
<body>
|
|
159
|
+
<div class="dashboard">
|
|
160
|
+
<div class="dashboard-header">
|
|
161
|
+
<h1>ClaudeTeach</h1>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="belt-display">
|
|
165
|
+
<span class="belt-icon" id="beltIcon"></span>
|
|
166
|
+
<div class="belt-info">
|
|
167
|
+
<h2 id="beltName">White Belt</h2>
|
|
168
|
+
<span class="xp" id="xpDisplay">0 XP</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="progress-bar-container">
|
|
173
|
+
<div class="progress-bar-fill" id="progressFill" style="width: 0%"></div>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="progress-label">
|
|
176
|
+
<span id="currentBeltLabel">White (0 XP)</span>
|
|
177
|
+
<span id="nextBeltLabel">Yellow (50 XP)</span>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="stats-grid">
|
|
181
|
+
<div class="stat-card">
|
|
182
|
+
<div class="stat-value" id="totalXp">0</div>
|
|
183
|
+
<div class="stat-label">Total XP</div>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="stat-card">
|
|
186
|
+
<div class="stat-value" id="modulesCompleted">0</div>
|
|
187
|
+
<div class="stat-label">Modules Done</div>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="stat-card">
|
|
190
|
+
<div class="stat-value" id="quizScore">-</div>
|
|
191
|
+
<div class="stat-label">Best Quiz</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<h3 class="section-title">Modules</h3>
|
|
196
|
+
<div class="module-list" id="moduleList">
|
|
197
|
+
<div class="module-card">
|
|
198
|
+
<div class="module-status locked">-</div>
|
|
199
|
+
<div class="module-info">
|
|
200
|
+
<div class="module-name">No modules loaded</div>
|
|
201
|
+
<div class="module-xp">Start a lesson to see progress</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div class="recommendation" id="recommendation">
|
|
207
|
+
<h3>Ready to learn?</h3>
|
|
208
|
+
<p>Start a lesson with <code>/teach <topic></code> in Claude Code</p>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<script>
|
|
213
|
+
/* --- Bridge connection --- */
|
|
214
|
+
var wsBridge = null;
|
|
215
|
+
(function connectBridge() {
|
|
216
|
+
try {
|
|
217
|
+
wsBridge = new WebSocket('ws://' + location.host + '/ws');
|
|
218
|
+
wsBridge.onopen = function() {
|
|
219
|
+
wsBridge.send(JSON.stringify({
|
|
220
|
+
v: 1, type: 'sys:connect',
|
|
221
|
+
payload: { clientType: 'template' },
|
|
222
|
+
source: 'template', timestamp: Date.now()
|
|
223
|
+
}));
|
|
224
|
+
};
|
|
225
|
+
wsBridge.onmessage = function(event) {
|
|
226
|
+
try {
|
|
227
|
+
var msg = JSON.parse(event.data);
|
|
228
|
+
if (msg.type === 'canvas:dashboard' && msg.payload) {
|
|
229
|
+
updateDashboard(msg.payload);
|
|
230
|
+
}
|
|
231
|
+
} catch(e) {}
|
|
232
|
+
};
|
|
233
|
+
wsBridge.onerror = function() { wsBridge = null; };
|
|
234
|
+
wsBridge.onclose = function() { wsBridge = null; };
|
|
235
|
+
} catch(e) { wsBridge = null; }
|
|
236
|
+
})();
|
|
237
|
+
|
|
238
|
+
// Also poll REST for progress data
|
|
239
|
+
(function pollProgress() {
|
|
240
|
+
fetch('/api/progress').then(function(r) { return r.json(); }).then(function(data) {
|
|
241
|
+
if (data && data.progress) updateDashboard(data);
|
|
242
|
+
}).catch(function(){});
|
|
243
|
+
})();
|
|
244
|
+
|
|
245
|
+
const BELTS = [
|
|
246
|
+
{ name: 'White', minXP: 0, badge: '\u2B1C' },
|
|
247
|
+
{ name: 'Yellow', minXP: 50, badge: '\uD83D\uDFE8' },
|
|
248
|
+
{ name: 'Green', minXP: 150, badge: '\uD83D\uDFE9' },
|
|
249
|
+
{ name: 'Blue', minXP: 400, badge: '\uD83D\uDFE6' },
|
|
250
|
+
{ name: 'Purple', minXP: 800, badge: '\uD83D\uDFEA' },
|
|
251
|
+
{ name: 'Brown', minXP: 1500, badge: '\uD83D\uDFEB' },
|
|
252
|
+
{ name: 'Black', minXP: 3000, badge: '\u2B1B' },
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
function getBelt(xp) {
|
|
256
|
+
let belt = BELTS[0];
|
|
257
|
+
for (const b of BELTS) {
|
|
258
|
+
if (xp >= b.minXP) belt = b;
|
|
259
|
+
}
|
|
260
|
+
return belt;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getNextBelt(xp) {
|
|
264
|
+
for (const b of BELTS) {
|
|
265
|
+
if (xp < b.minXP) return b;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function updateDashboard(data) {
|
|
271
|
+
const progress = data.progress || {};
|
|
272
|
+
const xp = progress.xp || 0;
|
|
273
|
+
const belt = getBelt(xp);
|
|
274
|
+
const next = getNextBelt(xp);
|
|
275
|
+
|
|
276
|
+
document.getElementById('beltIcon').textContent = belt.badge;
|
|
277
|
+
document.getElementById('beltName').textContent = belt.name + ' Belt';
|
|
278
|
+
document.getElementById('xpDisplay').textContent = xp + ' XP';
|
|
279
|
+
document.getElementById('totalXp').textContent = xp;
|
|
280
|
+
|
|
281
|
+
if (next) {
|
|
282
|
+
const pct = Math.min(100, ((xp - belt.minXP) / (next.minXP - belt.minXP)) * 100);
|
|
283
|
+
document.getElementById('progressFill').style.width = pct + '%';
|
|
284
|
+
document.getElementById('currentBeltLabel').textContent = `${belt.name} (${belt.minXP} XP)`;
|
|
285
|
+
document.getElementById('nextBeltLabel').textContent = `${next.name} (${next.minXP} XP)`;
|
|
286
|
+
} else {
|
|
287
|
+
document.getElementById('progressFill').style.width = '100%';
|
|
288
|
+
document.getElementById('currentBeltLabel').textContent = `${belt.name} Belt`;
|
|
289
|
+
document.getElementById('nextBeltLabel').textContent = 'Max level!';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Modules
|
|
293
|
+
const modules = progress.modules || [];
|
|
294
|
+
const completedCount = modules.filter(m => m.status === 'completed').length;
|
|
295
|
+
document.getElementById('modulesCompleted').textContent = completedCount;
|
|
296
|
+
|
|
297
|
+
if (modules.length > 0) {
|
|
298
|
+
const list = document.getElementById('moduleList');
|
|
299
|
+
list.innerHTML = '';
|
|
300
|
+
for (const mod of modules) {
|
|
301
|
+
const statusClass = mod.status || 'locked';
|
|
302
|
+
const statusIcon = statusClass === 'completed' ? '\u2713' :
|
|
303
|
+
statusClass === 'in-progress' ? '\u25D0' : '-';
|
|
304
|
+
list.innerHTML += `
|
|
305
|
+
<div class="module-card">
|
|
306
|
+
<div class="module-status ${statusClass}">${statusIcon}</div>
|
|
307
|
+
<div class="module-info">
|
|
308
|
+
<div class="module-name">${mod.title || mod.slug || 'Module'}</div>
|
|
309
|
+
<div class="module-xp">${mod.xp || 0} XP earned</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Recommendation
|
|
317
|
+
if (data.recommendation) {
|
|
318
|
+
const rec = data.recommendation;
|
|
319
|
+
document.getElementById('recommendation').innerHTML = `
|
|
320
|
+
<h3>Recommended: ${rec.title || rec.slug}</h3>
|
|
321
|
+
<p>${rec.description || 'Continue your learning journey'}</p>
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
window.addEventListener('message', (e) => {
|
|
327
|
+
if (e.data && e.data.type === 'dashboard-data') {
|
|
328
|
+
updateDashboard(e.data.payload);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (window.__DASHBOARD_DATA__) {
|
|
333
|
+
updateDashboard(window.__DASHBOARD_DATA__);
|
|
334
|
+
}
|
|
335
|
+
</script>
|
|
336
|
+
</body>
|
|
337
|
+
</html>
|