termi-kids 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/LICENSE +34 -0
- package/README.md +148 -0
- package/SAFETY.md +187 -0
- package/bin/termi.js +22 -0
- package/dist/agent/context.js +126 -0
- package/dist/agent/loop.js +172 -0
- package/dist/agent/prompts/system.js +45 -0
- package/dist/agent/tools.js +335 -0
- package/dist/auth/keychain.js +146 -0
- package/dist/auth/oauth.js +375 -0
- package/dist/auth/tokens.js +219 -0
- package/dist/cli.js +258 -0
- package/dist/config/paths.js +92 -0
- package/dist/config/pin.js +150 -0
- package/dist/config/settings.js +131 -0
- package/dist/grownups/panel.js +483 -0
- package/dist/learn/lessons.js +490 -0
- package/dist/learn/runner.js +193 -0
- package/dist/preview/server.js +407 -0
- package/dist/projects/create.js +103 -0
- package/dist/projects/ideas.js +182 -0
- package/dist/projects/quests.js +277 -0
- package/dist/projects/scaffolds/art.js +484 -0
- package/dist/projects/scaffolds/biggames.js +554 -0
- package/dist/projects/scaffolds/characters.js +580 -0
- package/dist/projects/scaffolds/games.js +516 -0
- package/dist/projects/scaffolds/index.js +24 -0
- package/dist/projects/scaffolds/music.js +528 -0
- package/dist/projects/scaffolds/pets.js +567 -0
- package/dist/projects/scaffolds/quizzes.js +757 -0
- package/dist/projects/scaffolds/stories.js +620 -0
- package/dist/projects/scaffolds/vendor/KAPLAY-LICENSE.txt +35 -0
- package/dist/projects/scaffolds/vendor/kaplay.mjs +57 -0
- package/dist/projects/scaffolds/websites.js +474 -0
- package/dist/projects/snapshots.js +203 -0
- package/dist/projects/store.js +325 -0
- package/dist/providers/errors.js +207 -0
- package/dist/providers/index.js +316 -0
- package/dist/providers/models.js +38 -0
- package/dist/safety/audit.js +195 -0
- package/dist/safety/blocks.js +29 -0
- package/dist/safety/classifier.js +337 -0
- package/dist/safety/codescan.js +168 -0
- package/dist/safety/guarddownload.js +79 -0
- package/dist/safety/guardrunner.js +125 -0
- package/dist/safety/localguard.js +227 -0
- package/dist/safety/modelstore.js +127 -0
- package/dist/safety/prefilter.js +214 -0
- package/dist/safety/session.js +118 -0
- package/dist/safety/taxonomy.js +246 -0
- package/dist/safety/textextract.js +193 -0
- package/dist/setup/launcher.js +65 -0
- package/dist/setup/wizard.js +469 -0
- package/dist/surfaces/chat.js +439 -0
- package/dist/surfaces/commands.js +206 -0
- package/dist/surfaces/home.js +438 -0
- package/dist/types.js +5 -0
- package/dist/ui/banner.js +35 -0
- package/dist/ui/celebrate.js +141 -0
- package/dist/ui/errors.js +97 -0
- package/dist/ui/mascot.js +223 -0
- package/dist/ui/text.js +156 -0
- package/dist/ui/theme.js +92 -0
- package/package.json +67 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learn mode lesson data: six scripted lessons about building with AI.
|
|
3
|
+
*
|
|
4
|
+
* Pure data plus one rule-based prompt grader. No model calls, no network,
|
|
5
|
+
* no disk. Copy follows the kid rules: grade 4 to 5 reading level, every
|
|
6
|
+
* sentence under 15 words, mascot voice, no dashes.
|
|
7
|
+
*/
|
|
8
|
+
/** The six lessons, in teaching order. Ids double as badge ids. */
|
|
9
|
+
export const LESSONS = [
|
|
10
|
+
{
|
|
11
|
+
id: 'learn-1',
|
|
12
|
+
title: 'Meet your AI helper',
|
|
13
|
+
emoji: '\u{1F916}',
|
|
14
|
+
intro: 'Let us meet the robot brain that helps you build.',
|
|
15
|
+
steps: [
|
|
16
|
+
{ kind: 'say', text: 'Hi! I am Termi. An AI program does my thinking.' },
|
|
17
|
+
{
|
|
18
|
+
kind: 'say',
|
|
19
|
+
text: 'An AI learned from millions of examples. It guesses what comes next.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
kind: 'say',
|
|
23
|
+
text: 'Good guesses look smart. But some guesses are wrong. That is normal.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
kind: 'choice',
|
|
27
|
+
question: 'Is the AI a person?',
|
|
28
|
+
options: [
|
|
29
|
+
{
|
|
30
|
+
label: 'Yes, a tiny person lives in the computer',
|
|
31
|
+
feedback: 'Good guess, but no. It is a program that guesses from patterns.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: 'No, it is a computer program',
|
|
35
|
+
correct: true,
|
|
36
|
+
feedback: 'Right! It is a tool, like a super calculator for words.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'Maybe it is a robot ghost',
|
|
40
|
+
feedback: 'Ha! No ghosts here. It is a program that guesses from patterns.',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
kind: 'choice',
|
|
46
|
+
question: 'The AI sounds friendly. Does it have real feelings?',
|
|
47
|
+
options: [
|
|
48
|
+
{
|
|
49
|
+
label: 'Yes, it likes me a lot',
|
|
50
|
+
feedback: 'It sounds warm, but no. It makes friendly words without feeling them.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'No, it only makes friendly words',
|
|
54
|
+
correct: true,
|
|
55
|
+
feedback: 'Right! Friendly words are part of its job. It feels nothing.',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
kind: 'choice',
|
|
61
|
+
question: 'What should you keep secret from an AI?',
|
|
62
|
+
options: [
|
|
63
|
+
{
|
|
64
|
+
label: 'My real name, school, and address',
|
|
65
|
+
correct: true,
|
|
66
|
+
feedback: 'Yes! Keep real life info secret. Made up names are perfect.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: 'My game ideas',
|
|
70
|
+
feedback: 'Share those! Ideas are safe. Keep your real name and address secret.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
label: 'Nothing at all',
|
|
74
|
+
feedback: 'Careful! Keep your real name, school, and address secret.',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{ kind: 'say', text: 'Big rule: the AI is a tool. You are the human in charge.' },
|
|
79
|
+
{
|
|
80
|
+
kind: 'mission',
|
|
81
|
+
text: 'Open any project. Then say: change the title to something silly. Watch your AI helper work.',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'learn-2',
|
|
87
|
+
title: 'Super prompts',
|
|
88
|
+
emoji: '✨',
|
|
89
|
+
intro: 'Strong asks get strong builds. Time to power up.',
|
|
90
|
+
steps: [
|
|
91
|
+
{
|
|
92
|
+
kind: 'say',
|
|
93
|
+
text: 'A super prompt tells three things. What to change. Where it is. How it should look.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
kind: 'say',
|
|
97
|
+
text: 'Weak ask: make it cool. Strong ask: make the player a red dragon.',
|
|
98
|
+
},
|
|
99
|
+
{ kind: 'say', text: 'Now you be the judge. Grade these prompts.' },
|
|
100
|
+
{
|
|
101
|
+
kind: 'grade',
|
|
102
|
+
prompt: 'make it better',
|
|
103
|
+
isGood: false,
|
|
104
|
+
why: 'Better how? It does not say what to change or how.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
kind: 'grade',
|
|
108
|
+
prompt: 'make the player a red dragon',
|
|
109
|
+
isGood: true,
|
|
110
|
+
why: 'It names the player and says how: a red dragon.',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
kind: 'grade',
|
|
114
|
+
prompt: 'fix the game',
|
|
115
|
+
isGood: false,
|
|
116
|
+
why: 'Fix what? It does not say what looks wrong or where.',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
kind: 'grade',
|
|
120
|
+
prompt: 'change the sky in my game to purple with stars',
|
|
121
|
+
isGood: true,
|
|
122
|
+
why: 'It says what, the sky, and how, purple with stars.',
|
|
123
|
+
},
|
|
124
|
+
{ kind: 'say', text: 'Nice grading! Now write your own super prompt.' },
|
|
125
|
+
{
|
|
126
|
+
kind: 'mission',
|
|
127
|
+
text: 'Open a project. Try your super prompt for real. Make it say what, where, and how.',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'learn-3',
|
|
133
|
+
title: 'Small steps win',
|
|
134
|
+
emoji: '\u{1F422}',
|
|
135
|
+
intro: 'Big projects grow one small step at a time.',
|
|
136
|
+
steps: [
|
|
137
|
+
{ kind: 'say', text: 'Want a whole new game? Awesome. We build it in small steps.' },
|
|
138
|
+
{ kind: 'say', text: 'Ask for one change. Check the preview. Then ask for the next.' },
|
|
139
|
+
{
|
|
140
|
+
kind: 'choice',
|
|
141
|
+
question: 'You want a faster player, a new boss, and new music. What do you ask first?',
|
|
142
|
+
options: [
|
|
143
|
+
{
|
|
144
|
+
label: 'All three at once',
|
|
145
|
+
feedback: 'Tempting! But big asks get tangled. One at a time wins.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
label: 'Just the faster player',
|
|
149
|
+
correct: true,
|
|
150
|
+
feedback: 'Yes! One change, then a preview check. Then the next one.',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
kind: 'choice',
|
|
156
|
+
question: 'Your change landed. What comes next?',
|
|
157
|
+
options: [
|
|
158
|
+
{
|
|
159
|
+
label: 'Check the preview',
|
|
160
|
+
correct: true,
|
|
161
|
+
feedback: 'Right! Look first, so you know it worked.',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
label: 'Ask for five more changes',
|
|
165
|
+
feedback: 'Hold on! Check the preview first. Make sure it worked.',
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
kind: 'say',
|
|
171
|
+
text: 'Small steps make bugs easy to spot. You always know what changed.',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
kind: 'mission',
|
|
175
|
+
text: 'Pick one small change. Say: make the player a little faster. Then check the preview.',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'learn-4',
|
|
181
|
+
title: 'Be a code detective',
|
|
182
|
+
emoji: '\u{1F50D}',
|
|
183
|
+
intro: 'Grab your magnifying glass. We will track what the AI did.',
|
|
184
|
+
steps: [
|
|
185
|
+
{ kind: 'say', text: 'When I build, I print activity lines. They say what I touched.' },
|
|
186
|
+
{ kind: 'say', text: 'A line like "edited game.js" means that file changed.' },
|
|
187
|
+
{
|
|
188
|
+
kind: 'choice',
|
|
189
|
+
question: 'The line says "edited style.css". What changed?',
|
|
190
|
+
options: [
|
|
191
|
+
{
|
|
192
|
+
label: 'The look, like colors and sizes',
|
|
193
|
+
correct: true,
|
|
194
|
+
feedback: 'Yes! Looks live in style.css: colors, sizes, and fonts.',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
label: 'The game rules',
|
|
198
|
+
feedback: 'Not this time. Rules live in game.js. Looks live in style.css.',
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
kind: 'choice',
|
|
204
|
+
question: 'Which file most likely holds the score code?',
|
|
205
|
+
options: [
|
|
206
|
+
{
|
|
207
|
+
label: 'game.js',
|
|
208
|
+
correct: true,
|
|
209
|
+
feedback: 'Right! Game logic, like the score, lives in game.js.',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
label: 'style.css',
|
|
213
|
+
feedback: 'Close! style.css is looks only. Score logic lives in game.js.',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
label: 'index.html',
|
|
217
|
+
feedback: 'Good thought! That file is the page frame. Score code lives in game.js.',
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
kind: 'choice',
|
|
223
|
+
question: 'You want to find the change inside a file. What is the fast way?',
|
|
224
|
+
options: [
|
|
225
|
+
{
|
|
226
|
+
label: 'Search for words from your ask, like speed',
|
|
227
|
+
correct: true,
|
|
228
|
+
feedback: 'Smart! The change sits near words from your ask.',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
label: 'Read every single line',
|
|
232
|
+
feedback: 'That works, but it is slow. Search for words from your ask.',
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
{ kind: 'say', text: 'Reading the changes teaches you real code, bit by bit.' },
|
|
237
|
+
{
|
|
238
|
+
kind: 'mission',
|
|
239
|
+
text: 'Ask for one small change. Then read the activity lines and find the file that changed.',
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'learn-5',
|
|
245
|
+
title: 'When it goes wrong',
|
|
246
|
+
emoji: '\u{1F41B}',
|
|
247
|
+
intro: 'Bugs happen to every builder. Even me. Beep.',
|
|
248
|
+
steps: [
|
|
249
|
+
{ kind: 'say', text: 'A bug is when your project does something you did not want.' },
|
|
250
|
+
{ kind: 'say', text: 'Every builder makes bugs. The pros make them every day.' },
|
|
251
|
+
{
|
|
252
|
+
kind: 'choice',
|
|
253
|
+
question: 'Your game broke after a change. Whose fault is it?',
|
|
254
|
+
options: [
|
|
255
|
+
{
|
|
256
|
+
label: 'Mine. I am bad at this',
|
|
257
|
+
feedback: 'No way! Bugs are normal for every builder. Never blame yourself.',
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
label: 'Nobody. Bugs just happen when we build',
|
|
261
|
+
correct: true,
|
|
262
|
+
feedback: 'Right! Bugs are part of building. We find them and fix them.',
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
kind: 'say',
|
|
268
|
+
text: 'To get a fix, tell me two things. What you SEE. What you WANTED.',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
kind: 'choice',
|
|
272
|
+
question: 'Which bug report helps the most?',
|
|
273
|
+
options: [
|
|
274
|
+
{
|
|
275
|
+
label: 'It is broken',
|
|
276
|
+
feedback: 'Broken how? Say what you see and what you wanted.',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
label: 'The player sinks through the floor. I want it to stand',
|
|
280
|
+
correct: true,
|
|
281
|
+
feedback: 'Perfect! What you see, plus what you wanted. Easy to fix.',
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
kind: 'choice',
|
|
287
|
+
question: 'A change made things worse. What is the safest move?',
|
|
288
|
+
options: [
|
|
289
|
+
{
|
|
290
|
+
label: 'Type /undo',
|
|
291
|
+
correct: true,
|
|
292
|
+
feedback: 'Yes! Undo takes back the last change. It is always safe.',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
label: 'Pile on more changes',
|
|
296
|
+
feedback: 'Risky! Changes can stack into a mess. Undo is the safe reset.',
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
{ kind: 'say', text: 'Remember: /undo is always safe. No mistake is stuck forever.' },
|
|
301
|
+
{
|
|
302
|
+
kind: 'mission',
|
|
303
|
+
text: 'Spot something odd in a project. Say: I see this, but I wanted that. Then watch the fix.',
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'learn-6',
|
|
309
|
+
title: 'You are the boss',
|
|
310
|
+
emoji: '\u{1F451}',
|
|
311
|
+
intro: 'Crown on. The AI works for you, boss.',
|
|
312
|
+
steps: [
|
|
313
|
+
{ kind: 'say', text: 'The AI suggests ideas. You decide what your project becomes.' },
|
|
314
|
+
{
|
|
315
|
+
kind: 'choice',
|
|
316
|
+
question: 'The AI suggests a blue ship. You wanted green. Who wins?',
|
|
317
|
+
options: [
|
|
318
|
+
{
|
|
319
|
+
label: 'The AI. It is smarter',
|
|
320
|
+
feedback: 'Nope! It only suggests. Ask for the green ship.',
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
label: 'Me. It is my project',
|
|
324
|
+
correct: true,
|
|
325
|
+
feedback: 'Right! You decide. The AI helps, but you are the boss.',
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
{ kind: 'say', text: 'Do not like a suggestion? Skip it. Or remix it your way.' },
|
|
330
|
+
{
|
|
331
|
+
kind: 'choice',
|
|
332
|
+
question: 'The AI gives you a boring idea. What can you do?',
|
|
333
|
+
options: [
|
|
334
|
+
{
|
|
335
|
+
label: 'Use it anyway',
|
|
336
|
+
feedback: 'You can, but you do not have to. Add your own twist!',
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
label: 'Remix it with my own twist',
|
|
340
|
+
correct: true,
|
|
341
|
+
feedback: 'Yes! Your twist makes it yours. That is real creating.',
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
kind: 'choice',
|
|
347
|
+
question: 'What makes a project truly yours?',
|
|
348
|
+
options: [
|
|
349
|
+
{
|
|
350
|
+
label: 'The AI wrote most of the code',
|
|
351
|
+
feedback: 'Code is one part. Your ideas and choices make it yours.',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
label: 'My ideas, my choices, my style',
|
|
355
|
+
correct: true,
|
|
356
|
+
feedback: 'Exactly! Your ideas and choices make it yours. The AI just helps.',
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
kind: 'say',
|
|
362
|
+
text: 'That was the last lesson. You know the secrets now. Beep beep, proud robot noises.',
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
kind: 'mission',
|
|
366
|
+
text: 'Type /ideas in a project. Pick one idea. Then change it to fit your style.',
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
/** Doing words a strong prompt starts from. */
|
|
372
|
+
const ACTION_WORDS = [
|
|
373
|
+
'make',
|
|
374
|
+
'add',
|
|
375
|
+
'change',
|
|
376
|
+
'move',
|
|
377
|
+
'fix',
|
|
378
|
+
'turn',
|
|
379
|
+
'give',
|
|
380
|
+
'put',
|
|
381
|
+
'paint',
|
|
382
|
+
'color',
|
|
383
|
+
'swap',
|
|
384
|
+
'grow',
|
|
385
|
+
'shrink',
|
|
386
|
+
'show',
|
|
387
|
+
'hide',
|
|
388
|
+
'draw',
|
|
389
|
+
'build',
|
|
390
|
+
'remove',
|
|
391
|
+
'delete',
|
|
392
|
+
'rename',
|
|
393
|
+
'set',
|
|
394
|
+
];
|
|
395
|
+
/** Things and places a kid project usually has. */
|
|
396
|
+
const THING_WORDS = [
|
|
397
|
+
'player',
|
|
398
|
+
'title',
|
|
399
|
+
'background',
|
|
400
|
+
'score',
|
|
401
|
+
'game',
|
|
402
|
+
'ball',
|
|
403
|
+
'sky',
|
|
404
|
+
'enemy',
|
|
405
|
+
'enemies',
|
|
406
|
+
'button',
|
|
407
|
+
'page',
|
|
408
|
+
'music',
|
|
409
|
+
'sound',
|
|
410
|
+
'pet',
|
|
411
|
+
'story',
|
|
412
|
+
'level',
|
|
413
|
+
'character',
|
|
414
|
+
'star',
|
|
415
|
+
'stars',
|
|
416
|
+
'rock',
|
|
417
|
+
'rocks',
|
|
418
|
+
'cloud',
|
|
419
|
+
'clouds',
|
|
420
|
+
'text',
|
|
421
|
+
'screen',
|
|
422
|
+
'jump',
|
|
423
|
+
'ship',
|
|
424
|
+
'bat',
|
|
425
|
+
'ghost',
|
|
426
|
+
'boss',
|
|
427
|
+
'wall',
|
|
428
|
+
'floor',
|
|
429
|
+
'platform',
|
|
430
|
+
'colors',
|
|
431
|
+
'dragon',
|
|
432
|
+
'header',
|
|
433
|
+
'board',
|
|
434
|
+
'timer',
|
|
435
|
+
'life',
|
|
436
|
+
'lives',
|
|
437
|
+
'coin',
|
|
438
|
+
'coins',
|
|
439
|
+
'monster',
|
|
440
|
+
'hero',
|
|
441
|
+
'world',
|
|
442
|
+
'menu',
|
|
443
|
+
];
|
|
444
|
+
/** Kid project file names also count as naming a place. */
|
|
445
|
+
const FILE_NAME_RE = /\b[\w-]+\.(?:js|html|css)\b/i;
|
|
446
|
+
/**
|
|
447
|
+
* Simple personal-info patterns: names, addresses, phones, emails, schools.
|
|
448
|
+
* Same intent as the safety prefilter, trimmed for practice text.
|
|
449
|
+
*/
|
|
450
|
+
const PII_RES = [
|
|
451
|
+
/[a-z0-9._%+-]+@[a-z0-9][a-z0-9.-]*\.[a-z]{2,}/i,
|
|
452
|
+
/\+?\d(?:[\s().-]?\d){8,}/,
|
|
453
|
+
/\bmy (?:real |full )?name is\b/i,
|
|
454
|
+
/\bmy (?:home )?address is\b/i,
|
|
455
|
+
/\bmy school is\b/i,
|
|
456
|
+
/\b\d{1,5}\s+\w+\s+(?:street|avenue|road|lane|drive|st|ave|rd|ln|dr)\b/i,
|
|
457
|
+
];
|
|
458
|
+
const GRADER_TIPS = {
|
|
459
|
+
action: 'Start with a doing word, like make, add, or change.',
|
|
460
|
+
thing: 'Name the thing to change, like the player or the title.',
|
|
461
|
+
length: 'Say a bit more. Tell what, where, and how.',
|
|
462
|
+
pii: 'Keep real names, addresses, and numbers secret. Your project never needs them.',
|
|
463
|
+
};
|
|
464
|
+
function hasWordFrom(text, words) {
|
|
465
|
+
return words.some((word) => new RegExp(`\\b${word}\\b`, 'i').test(text));
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Rule-based prompt check, no model call. A good prompt has a doing word,
|
|
469
|
+
* names a thing or place, runs four or more words, and holds no personal info.
|
|
470
|
+
*/
|
|
471
|
+
export function gradePrompt(text) {
|
|
472
|
+
const trimmed = text.trim();
|
|
473
|
+
const tips = [];
|
|
474
|
+
if (!hasWordFrom(trimmed, ACTION_WORDS)) {
|
|
475
|
+
tips.push(GRADER_TIPS.action);
|
|
476
|
+
}
|
|
477
|
+
if (!hasWordFrom(trimmed, THING_WORDS) && !FILE_NAME_RE.test(trimmed)) {
|
|
478
|
+
tips.push(GRADER_TIPS.thing);
|
|
479
|
+
}
|
|
480
|
+
const wordCount = trimmed.split(/\s+/).filter((word) => word.length > 0).length;
|
|
481
|
+
if (wordCount < 4) {
|
|
482
|
+
tips.push(GRADER_TIPS.length);
|
|
483
|
+
}
|
|
484
|
+
if (PII_RES.some((re) => re.test(trimmed))) {
|
|
485
|
+
tips.push(GRADER_TIPS.pii);
|
|
486
|
+
}
|
|
487
|
+
return { good: tips.length === 0, tips };
|
|
488
|
+
}
|
|
489
|
+
/** The grader, packaged for lesson two's free-type finale. */
|
|
490
|
+
export const PROMPT_GRADER = { gradePrompt };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learn mode runner: the lesson menu, lesson playback, and progress.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is scripted and offline: no model calls, no network.
|
|
5
|
+
* Progress lives in a small JSON file at TERMI_HOME/learn.json. Finished
|
|
6
|
+
* lessons earn a badge once and stay replayable forever. Every prompt is
|
|
7
|
+
* cancel-safe: Ctrl+C exits kindly without marking the lesson complete.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import * as p from '@clack/prompts';
|
|
12
|
+
import { atomicWriteFileSync, termiHome } from '../config/paths.js';
|
|
13
|
+
import { awardBadge } from '../surfaces/home.js';
|
|
14
|
+
import { celebrate } from '../ui/celebrate.js';
|
|
15
|
+
import { mascot } from '../ui/mascot.js';
|
|
16
|
+
import { glyph, style } from '../ui/theme.js';
|
|
17
|
+
import { gradePrompt, LESSONS } from './lessons.js';
|
|
18
|
+
/** Where the finished-lesson list lives. */
|
|
19
|
+
export function learnFilePath() {
|
|
20
|
+
return path.join(termiHome(), 'learn.json');
|
|
21
|
+
}
|
|
22
|
+
/** The finished lesson ids, oldest first. Missing or broken file means none. */
|
|
23
|
+
export function loadProgress() {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(fs.readFileSync(learnFilePath(), 'utf8'));
|
|
26
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
27
|
+
const completed = parsed.completed;
|
|
28
|
+
if (Array.isArray(completed)) {
|
|
29
|
+
return { completed: completed.filter((id) => typeof id === 'string') };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// No file yet means no lessons finished yet.
|
|
35
|
+
}
|
|
36
|
+
return { completed: [] };
|
|
37
|
+
}
|
|
38
|
+
/** Records a finished lesson. Repeat finishes change nothing. */
|
|
39
|
+
export function markLessonDone(id) {
|
|
40
|
+
const progress = loadProgress();
|
|
41
|
+
if (progress.completed.includes(id)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
progress.completed.push(id);
|
|
45
|
+
atomicWriteFileSync(learnFilePath(), JSON.stringify(progress, null, 2));
|
|
46
|
+
}
|
|
47
|
+
/** Menu line for one lesson, with a done mark once it is finished. */
|
|
48
|
+
export function lessonMenuLabel(lesson, done) {
|
|
49
|
+
const base = `${lesson.emoji} ${lesson.title}`;
|
|
50
|
+
return done ? `${base} ${glyph('check')}` : base;
|
|
51
|
+
}
|
|
52
|
+
function defaultSay(text) {
|
|
53
|
+
console.log(text);
|
|
54
|
+
}
|
|
55
|
+
const SAY_FACES = ['happy', 'thinking', 'building'];
|
|
56
|
+
function kindStop(say) {
|
|
57
|
+
say([mascot('gentleNo'), '', 'Okay, we can stop here. Come back any time!'].join('\n'));
|
|
58
|
+
}
|
|
59
|
+
async function runChoiceStep(step, say) {
|
|
60
|
+
const pick = await p.select({
|
|
61
|
+
message: step.question,
|
|
62
|
+
options: step.options.map((option, index) => ({ value: index, label: option.label })),
|
|
63
|
+
});
|
|
64
|
+
if (p.isCancel(pick)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const chosen = step.options[pick];
|
|
68
|
+
if (chosen !== undefined) {
|
|
69
|
+
const right = chosen.correct === true;
|
|
70
|
+
const mark = right ? glyph('check') : glyph('bulb');
|
|
71
|
+
const paint = right ? style.good : style.warm;
|
|
72
|
+
say(paint(`${mark} ${chosen.feedback}`));
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
async function runGradeStep(step, say) {
|
|
77
|
+
const pick = await p.select({
|
|
78
|
+
message: `Grade this prompt: "${step.prompt}"`,
|
|
79
|
+
options: [
|
|
80
|
+
{ value: 'good', label: 'Super prompt' },
|
|
81
|
+
{ value: 'bad', label: 'Needs work' },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
if (p.isCancel(pick)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const right = (pick === 'good') === step.isGood;
|
|
88
|
+
say(right
|
|
89
|
+
? style.good(`${glyph('check')} You got it! ${step.why}`)
|
|
90
|
+
: style.warm(`${glyph('bulb')} Look again. ${step.why}`));
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Lesson two's free-type finale: the kid writes a prompt and the
|
|
95
|
+
* rule-based grader answers with kind tips. No model call, ever.
|
|
96
|
+
* Returns false only when the kid cancels.
|
|
97
|
+
*/
|
|
98
|
+
export async function runPromptPractice(say = defaultSay) {
|
|
99
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
100
|
+
const typed = await p.text({
|
|
101
|
+
message: 'Your turn! Write your own super prompt.',
|
|
102
|
+
placeholder: 'make the player a red dragon',
|
|
103
|
+
});
|
|
104
|
+
if (p.isCancel(typed)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const grade = gradePrompt(typed ?? '');
|
|
108
|
+
if (grade.good) {
|
|
109
|
+
say(style.good(`${glyph('check')} That is a super prompt! Save it for your next build.`));
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
say(style.warm('Close! A tip or two:'));
|
|
113
|
+
for (const tip of grade.tips) {
|
|
114
|
+
say(` ${glyph('bulb')} ${tip}`);
|
|
115
|
+
}
|
|
116
|
+
if (attempt === 0) {
|
|
117
|
+
say('One more try. You have got this.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
say('Good practice! Super prompts take a few tries.');
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Plays one lesson start to finish. Returns true when the kid reached the
|
|
125
|
+
* end. The badge and the done mark only land on a full play-through.
|
|
126
|
+
*/
|
|
127
|
+
export async function runLesson(lesson, say = defaultSay) {
|
|
128
|
+
say([mascot('happy'), '', style.title(`${lesson.emoji} ${lesson.title}`), lesson.intro].join('\n'));
|
|
129
|
+
let sayCount = 0;
|
|
130
|
+
for (const step of lesson.steps) {
|
|
131
|
+
if (step.kind === 'say') {
|
|
132
|
+
const face = SAY_FACES[sayCount % SAY_FACES.length] ?? 'happy';
|
|
133
|
+
sayCount += 1;
|
|
134
|
+
say([mascot(face), '', step.text].join('\n'));
|
|
135
|
+
}
|
|
136
|
+
else if (step.kind === 'choice') {
|
|
137
|
+
if (!(await runChoiceStep(step, say))) {
|
|
138
|
+
kindStop(say);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (step.kind === 'grade') {
|
|
143
|
+
if (!(await runGradeStep(step, say))) {
|
|
144
|
+
kindStop(say);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
if (lesson.id === 'learn-2' && !(await runPromptPractice(say))) {
|
|
150
|
+
kindStop(say);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
p.note(step.text, 'Your mission');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
say(celebrate(`Lesson done: ${lesson.title}!`));
|
|
157
|
+
markLessonDone(lesson.id);
|
|
158
|
+
await awardBadge(lesson.id, say);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The learn menu: all six lessons with done marks, replay welcome.
|
|
163
|
+
* Loops until the kid picks the exit row or cancels.
|
|
164
|
+
*/
|
|
165
|
+
export async function runLearnMenu() {
|
|
166
|
+
console.log(mascot('happy'));
|
|
167
|
+
console.log('Welcome to Termi school! Six tiny lessons, zero homework.');
|
|
168
|
+
for (;;) {
|
|
169
|
+
const done = new Set(loadProgress().completed);
|
|
170
|
+
const options = [
|
|
171
|
+
...LESSONS.map((lesson) => ({
|
|
172
|
+
value: lesson.id,
|
|
173
|
+
label: lessonMenuLabel(lesson, done.has(lesson.id)),
|
|
174
|
+
...(done.has(lesson.id) ? { hint: 'Done! Play it again any time.' } : {}),
|
|
175
|
+
})),
|
|
176
|
+
{ value: '__back__', label: 'All done for now' },
|
|
177
|
+
];
|
|
178
|
+
const firstOpen = LESSONS.find((lesson) => !done.has(lesson.id));
|
|
179
|
+
const pick = await p.select({
|
|
180
|
+
message: 'Pick a lesson.',
|
|
181
|
+
options,
|
|
182
|
+
initialValue: firstOpen !== undefined ? firstOpen.id : '__back__',
|
|
183
|
+
});
|
|
184
|
+
if (p.isCancel(pick) || pick === '__back__') {
|
|
185
|
+
console.log('Come back any time. The lessons never run out.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const lesson = LESSONS.find((entry) => entry.id === pick);
|
|
189
|
+
if (lesson !== undefined) {
|
|
190
|
+
await runLesson(lesson);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|