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,757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quiz Show: one template, two modes picked by a QUIZ_TYPE const.
|
|
3
|
+
* Trivia mode: score, kind feedback, end grade. Personality mode:
|
|
4
|
+
* answers map to characters, fun result card. Kids edit plain arrays.
|
|
5
|
+
*/
|
|
6
|
+
function jsData(value) {
|
|
7
|
+
return JSON.stringify(value, null, 2);
|
|
8
|
+
}
|
|
9
|
+
function escapeHtml(text) {
|
|
10
|
+
return text
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"');
|
|
15
|
+
}
|
|
16
|
+
const animalsTheme = {
|
|
17
|
+
id: 'animals',
|
|
18
|
+
label: 'Animal Trivia',
|
|
19
|
+
emoji: '\u{1F43E}',
|
|
20
|
+
palette: { bg: '#14352a', fg: '#f2fff7', accent: '#ffd166' },
|
|
21
|
+
glyphs: { trophy: '\u{1F3C6}', star: '\u{2B50}', spark: '\u{2728}' },
|
|
22
|
+
strings: {
|
|
23
|
+
tagline: 'How well do you know the animal kingdom?',
|
|
24
|
+
praise1: 'Yes! You got it!',
|
|
25
|
+
praise2: 'Right answer! Nice one.',
|
|
26
|
+
praise3: 'Correct! You are on a roll.',
|
|
27
|
+
wrongLead: 'Almost!',
|
|
28
|
+
playAgain: 'Play again',
|
|
29
|
+
next: 'Next question',
|
|
30
|
+
finish: 'See my score',
|
|
31
|
+
bestLabel: 'Best so far',
|
|
32
|
+
resultLead: 'You are',
|
|
33
|
+
},
|
|
34
|
+
narrativeIntro: 'Octopus hearts, sleepy horses, speedy cheetahs. Time to test your animal smarts.',
|
|
35
|
+
nonViolent: true,
|
|
36
|
+
nonCompetitive: false,
|
|
37
|
+
};
|
|
38
|
+
const characterTheme = {
|
|
39
|
+
id: 'which-character',
|
|
40
|
+
label: 'Which Character Are You?',
|
|
41
|
+
emoji: '\u{1F3AD}',
|
|
42
|
+
palette: { bg: '#241b4d', fg: '#f6f1ff', accent: '#ff7ab6' },
|
|
43
|
+
glyphs: { trophy: '\u{1F31F}', star: '\u{2B50}', spark: '\u{1F389}' },
|
|
44
|
+
strings: {
|
|
45
|
+
tagline: 'Answer six questions. Meet your inner hero.',
|
|
46
|
+
praise1: 'Yes! You got it!',
|
|
47
|
+
praise2: 'Right answer! Nice one.',
|
|
48
|
+
praise3: 'Correct! You are on a roll.',
|
|
49
|
+
wrongLead: 'Almost!',
|
|
50
|
+
playAgain: 'Play again',
|
|
51
|
+
next: 'Next question',
|
|
52
|
+
finish: 'See my result',
|
|
53
|
+
bestLabel: 'Best so far',
|
|
54
|
+
resultLead: 'You are',
|
|
55
|
+
},
|
|
56
|
+
narrativeIntro: 'Four heroes, one quiz. Your answers reveal which one you are.',
|
|
57
|
+
nonViolent: true,
|
|
58
|
+
nonCompetitive: true,
|
|
59
|
+
};
|
|
60
|
+
const animalsData = {
|
|
61
|
+
quizType: 'trivia',
|
|
62
|
+
trivia: [
|
|
63
|
+
{
|
|
64
|
+
q: 'How many hearts does an octopus have?',
|
|
65
|
+
answers: ['One', 'Three', 'Eight'],
|
|
66
|
+
correct: 1,
|
|
67
|
+
fact: 'Two hearts pump to the gills. One pumps to the body.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
q: 'Which animal can sleep standing up?',
|
|
71
|
+
answers: ['Horse', 'Frog', 'Penguin'],
|
|
72
|
+
correct: 0,
|
|
73
|
+
fact: 'Horses can lock their leg joints while they snooze.',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
q: 'What do pandas eat most of all?',
|
|
77
|
+
answers: ['Fish', 'Bamboo', 'Berries'],
|
|
78
|
+
correct: 1,
|
|
79
|
+
fact: 'A panda can munch bamboo for half the day.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
q: 'Which of these birds cannot fly?',
|
|
83
|
+
answers: ['Owl', 'Ostrich', 'Robin'],
|
|
84
|
+
correct: 1,
|
|
85
|
+
fact: 'Ostriches cannot fly, but they sprint super fast.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
q: 'How do dolphins find food in dark water?',
|
|
89
|
+
answers: ['Echo sounds', 'Smell', 'Tiny maps'],
|
|
90
|
+
correct: 0,
|
|
91
|
+
fact: 'It is called echolocation. Clicks bounce back like sonar.',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
q: 'Which animal can regrow its tail?',
|
|
95
|
+
answers: ['Gecko', 'Goat', 'Goose'],
|
|
96
|
+
correct: 0,
|
|
97
|
+
fact: 'Many geckos drop their tails to escape, then grow new ones.',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
q: 'What is a group of lions called?',
|
|
101
|
+
answers: ['A pack', 'A pride', 'A pod'],
|
|
102
|
+
correct: 1,
|
|
103
|
+
fact: 'A pod is for dolphins. A pack is for wolves.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
q: 'Which is the tallest land animal?',
|
|
107
|
+
answers: ['Elephant', 'Giraffe', 'Moose'],
|
|
108
|
+
correct: 1,
|
|
109
|
+
fact: 'A giraffe can grow about as tall as a two story house.',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
personality: [
|
|
113
|
+
{
|
|
114
|
+
q: 'It is Saturday morning. What do you do first?',
|
|
115
|
+
answers: [
|
|
116
|
+
{ text: 'Build a pillow fort', character: 'otter' },
|
|
117
|
+
{ text: 'Read about volcanoes', character: 'owl' },
|
|
118
|
+
{ text: 'Race outside', character: 'cheetah' },
|
|
119
|
+
{ text: 'Water my plants', character: 'tortoise' },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
q: 'Pick a snack.',
|
|
124
|
+
answers: [
|
|
125
|
+
{ text: 'Anything I can share', character: 'otter' },
|
|
126
|
+
{ text: 'Brain food, like trail mix', character: 'owl' },
|
|
127
|
+
{ text: 'Something quick to grab', character: 'cheetah' },
|
|
128
|
+
{ text: 'A picnic I planned', character: 'tortoise' },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
q: 'Your team is losing. What do you do?',
|
|
133
|
+
answers: [
|
|
134
|
+
{ text: 'Crack a joke to cheer us up', character: 'otter' },
|
|
135
|
+
{ text: 'Study the other team', character: 'owl' },
|
|
136
|
+
{ text: 'Sprint even harder', character: 'cheetah' },
|
|
137
|
+
{ text: 'Stick to our plan', character: 'tortoise' },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
q: 'Pick a superpower.',
|
|
142
|
+
answers: [
|
|
143
|
+
{ text: 'Talking to animals', character: 'otter' },
|
|
144
|
+
{ text: 'Knowing every answer', character: 'owl' },
|
|
145
|
+
{ text: 'Super speed', character: 'cheetah' },
|
|
146
|
+
{ text: 'Never getting tired', character: 'tortoise' },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
q: 'Homework time. What is your style?',
|
|
151
|
+
answers: [
|
|
152
|
+
{ text: 'Turn it into a game', character: 'otter' },
|
|
153
|
+
{ text: 'Extra credit, please', character: 'owl' },
|
|
154
|
+
{ text: 'Done in ten minutes', character: 'cheetah' },
|
|
155
|
+
{ text: 'A little bit each day', character: 'tortoise' },
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
q: 'Pick a field trip.',
|
|
160
|
+
answers: [
|
|
161
|
+
{ text: 'The splash park', character: 'otter' },
|
|
162
|
+
{ text: 'The science museum', character: 'owl' },
|
|
163
|
+
{ text: 'The go kart track', character: 'cheetah' },
|
|
164
|
+
{ text: 'The quiet forest trail', character: 'tortoise' },
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
characters: {
|
|
169
|
+
otter: {
|
|
170
|
+
name: 'Sunny the Otter',
|
|
171
|
+
emoji: '\u{1F9A6}',
|
|
172
|
+
line: 'Playful and kind. You make every day feel like recess.',
|
|
173
|
+
},
|
|
174
|
+
owl: {
|
|
175
|
+
name: 'Sage the Owl',
|
|
176
|
+
emoji: '\u{1F989}',
|
|
177
|
+
line: 'Curious and wise. You ask the best questions.',
|
|
178
|
+
},
|
|
179
|
+
cheetah: {
|
|
180
|
+
name: 'Dash the Cheetah',
|
|
181
|
+
emoji: '\u{1F406}',
|
|
182
|
+
line: 'Fast and bold. You jump in first and learn on the run.',
|
|
183
|
+
},
|
|
184
|
+
tortoise: {
|
|
185
|
+
name: 'Pebble the Tortoise',
|
|
186
|
+
emoji: '\u{1F422}',
|
|
187
|
+
line: 'Calm and steady. You finish what you start.',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const characterData = {
|
|
192
|
+
quizType: 'personality',
|
|
193
|
+
trivia: [
|
|
194
|
+
{
|
|
195
|
+
q: 'What is the biggest planet in our solar system?',
|
|
196
|
+
answers: ['Jupiter', 'Mars', 'Saturn'],
|
|
197
|
+
correct: 0,
|
|
198
|
+
fact: 'More than 1,000 Earths could fit inside Jupiter.',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
q: 'How many sides does a hexagon have?',
|
|
202
|
+
answers: ['Five', 'Six', 'Seven'],
|
|
203
|
+
correct: 1,
|
|
204
|
+
fact: 'Bees build hexagons in their honeycombs.',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
q: 'What do you call a baby kangaroo?',
|
|
208
|
+
answers: ['A cub', 'A joey', 'A kit'],
|
|
209
|
+
correct: 1,
|
|
210
|
+
fact: "A joey rides in its mom's pouch.",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
q: 'Which gas do we breathe in to live?',
|
|
214
|
+
answers: ['Oxygen', 'Helium', 'Steam'],
|
|
215
|
+
correct: 0,
|
|
216
|
+
fact: 'Plants make oxygen for us. Thanks, plants!',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
q: 'What is the fastest land animal?',
|
|
220
|
+
answers: ['Cheetah', 'Horse', 'Hippo'],
|
|
221
|
+
correct: 0,
|
|
222
|
+
fact: 'Cheetahs can sprint near 70 miles per hour.',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
q: 'How many minutes are in one hour?',
|
|
226
|
+
answers: ['Sixty', 'Thirty', 'One hundred'],
|
|
227
|
+
correct: 0,
|
|
228
|
+
fact: 'And sixty seconds make one minute.',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
q: 'Which ocean is the biggest?',
|
|
232
|
+
answers: ['Atlantic', 'Pacific', 'Arctic'],
|
|
233
|
+
correct: 1,
|
|
234
|
+
fact: 'The Pacific covers about a third of Earth.',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
q: 'Which shape has no corners?',
|
|
238
|
+
answers: ['Circle', 'Square', 'Triangle'],
|
|
239
|
+
correct: 0,
|
|
240
|
+
fact: 'No corners, no edges, just smooth all around.',
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
personality: [
|
|
244
|
+
{
|
|
245
|
+
q: 'A dragon blocks the bridge. What do you do?',
|
|
246
|
+
answers: [
|
|
247
|
+
{ text: 'Stand tall and talk to it', character: 'nova' },
|
|
248
|
+
{ text: 'Build a dragon snack machine', character: 'pixel' },
|
|
249
|
+
{ text: 'Offer it soup and a nap', character: 'maple' },
|
|
250
|
+
{ text: 'Find a sneaky side path', character: 'comet' },
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
q: 'Pick your gear.',
|
|
255
|
+
answers: [
|
|
256
|
+
{ text: 'A shiny shield', character: 'nova' },
|
|
257
|
+
{ text: 'A gadget belt', character: 'pixel' },
|
|
258
|
+
{ text: 'A healing kit', character: 'maple' },
|
|
259
|
+
{ text: 'Springy boots', character: 'comet' },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
q: 'Your friend feels scared. What do you do?',
|
|
264
|
+
answers: [
|
|
265
|
+
{ text: 'Say: stay behind me', character: 'nova' },
|
|
266
|
+
{ text: 'Invent a bravery hat', character: 'pixel' },
|
|
267
|
+
{ text: 'Listen and sit with them', character: 'maple' },
|
|
268
|
+
{ text: 'Scout ahead so it is safe', character: 'comet' },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
q: 'Pick a hideout.',
|
|
273
|
+
answers: [
|
|
274
|
+
{ text: 'A castle tower', character: 'nova' },
|
|
275
|
+
{ text: 'A secret lab', character: 'pixel' },
|
|
276
|
+
{ text: 'A garden treehouse', character: 'maple' },
|
|
277
|
+
{ text: 'A cave behind a waterfall', character: 'comet' },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
q: 'The map is missing. What now?',
|
|
282
|
+
answers: [
|
|
283
|
+
{ text: 'Lead on, no map needed', character: 'nova' },
|
|
284
|
+
{ text: 'Build a compass from junk', character: 'pixel' },
|
|
285
|
+
{ text: 'Ask a local owl politely', character: 'maple' },
|
|
286
|
+
{ text: 'Climb high and look around', character: 'comet' },
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
q: 'Pick a victory snack.',
|
|
291
|
+
answers: [
|
|
292
|
+
{ text: 'A feast for the whole team', character: 'nova' },
|
|
293
|
+
{ text: 'Freeze dried star candy', character: 'pixel' },
|
|
294
|
+
{ text: 'Warm berry pie', character: 'maple' },
|
|
295
|
+
{ text: 'Trail mix on the go', character: 'comet' },
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
characters: {
|
|
300
|
+
nova: {
|
|
301
|
+
name: 'Captain Nova',
|
|
302
|
+
emoji: '\u{1F680}',
|
|
303
|
+
line: 'Brave and loyal. Your crew follows you anywhere.',
|
|
304
|
+
},
|
|
305
|
+
pixel: {
|
|
306
|
+
name: 'Pixel the Inventor',
|
|
307
|
+
emoji: '\u{1F916}',
|
|
308
|
+
line: 'Clever and curious. You fix problems with wild ideas.',
|
|
309
|
+
},
|
|
310
|
+
maple: {
|
|
311
|
+
name: 'Maple the Healer',
|
|
312
|
+
emoji: '\u{1F33F}',
|
|
313
|
+
line: 'Kind and patient. You notice when friends need help.',
|
|
314
|
+
},
|
|
315
|
+
comet: {
|
|
316
|
+
name: 'Comet the Scout',
|
|
317
|
+
emoji: '\u{1F31F}',
|
|
318
|
+
line: 'Quick and cheerful. You always find the secret path.',
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
function buildHtml(theme, prettyName) {
|
|
323
|
+
const s = theme.strings;
|
|
324
|
+
return `<!DOCTYPE html>
|
|
325
|
+
<html lang="en">
|
|
326
|
+
<head>
|
|
327
|
+
<meta charset="UTF-8">
|
|
328
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
329
|
+
<title>${escapeHtml(prettyName)}</title>
|
|
330
|
+
<link rel="icon" href="data:,">
|
|
331
|
+
<link rel="stylesheet" href="style.css">
|
|
332
|
+
<script src="game.js" defer></script>
|
|
333
|
+
</head>
|
|
334
|
+
<body>
|
|
335
|
+
<main class="frame">
|
|
336
|
+
<header>
|
|
337
|
+
<h1><span class="emoji">${theme.emoji}</span> ${escapeHtml(prettyName)}</h1>
|
|
338
|
+
<p class="tagline">${s['tagline'] ?? ''}</p>
|
|
339
|
+
</header>
|
|
340
|
+
<section id="quiz" class="card">
|
|
341
|
+
<div class="progress-row">
|
|
342
|
+
<span id="progress"></span>
|
|
343
|
+
<span id="best"></span>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="bar"><div id="bar-fill"></div></div>
|
|
346
|
+
<h2 id="question"></h2>
|
|
347
|
+
<div id="answers"></div>
|
|
348
|
+
<p id="feedback"></p>
|
|
349
|
+
<button id="next" class="big hidden" type="button">${s['next'] ?? 'Next'}</button>
|
|
350
|
+
</section>
|
|
351
|
+
<section id="end" class="card center hidden">
|
|
352
|
+
<div id="end-emoji"></div>
|
|
353
|
+
<h2 id="end-title"></h2>
|
|
354
|
+
<p id="end-copy"></p>
|
|
355
|
+
<button id="play-again" class="big" type="button">${s['playAgain'] ?? 'Play again'}</button>
|
|
356
|
+
</section>
|
|
357
|
+
<p class="tip">Edit your questions in game.js. Flip QUIZ_TYPE to change the whole show!</p>
|
|
358
|
+
</main>
|
|
359
|
+
</body>
|
|
360
|
+
</html>
|
|
361
|
+
`;
|
|
362
|
+
}
|
|
363
|
+
function buildCss(theme) {
|
|
364
|
+
const p = theme.palette;
|
|
365
|
+
return `/* Quiz Show styles. Colors come from your THEME in game.js. */
|
|
366
|
+
* { box-sizing: border-box; }
|
|
367
|
+
:root {
|
|
368
|
+
--bg: ${p.bg};
|
|
369
|
+
--fg: ${p.fg};
|
|
370
|
+
--accent: ${p.accent};
|
|
371
|
+
}
|
|
372
|
+
body {
|
|
373
|
+
margin: 0;
|
|
374
|
+
min-height: 100vh;
|
|
375
|
+
background: var(--bg);
|
|
376
|
+
background-image: radial-gradient(circle at 80% 0%, color-mix(in srgb, var(--accent) 18%, var(--bg)), var(--bg) 60%);
|
|
377
|
+
color: var(--fg);
|
|
378
|
+
font-family: system-ui, "Segoe UI", Roboto, Arial, sans-serif;
|
|
379
|
+
display: flex;
|
|
380
|
+
justify-content: center;
|
|
381
|
+
padding: 28px 16px;
|
|
382
|
+
}
|
|
383
|
+
.frame { width: min(640px, 100%); }
|
|
384
|
+
header h1 { font-size: 1.7rem; margin: 0 0 4px; }
|
|
385
|
+
.tagline { margin: 0 0 18px; opacity: 0.85; }
|
|
386
|
+
.card {
|
|
387
|
+
background: rgba(255, 255, 255, 0.07);
|
|
388
|
+
background: color-mix(in srgb, var(--fg) 8%, var(--bg));
|
|
389
|
+
border: 1px solid color-mix(in srgb, var(--fg) 16%, var(--bg));
|
|
390
|
+
border-radius: 16px;
|
|
391
|
+
padding: 22px;
|
|
392
|
+
}
|
|
393
|
+
.center { text-align: center; }
|
|
394
|
+
.progress-row {
|
|
395
|
+
display: flex;
|
|
396
|
+
justify-content: space-between;
|
|
397
|
+
font-size: 0.9rem;
|
|
398
|
+
opacity: 0.85;
|
|
399
|
+
margin-bottom: 8px;
|
|
400
|
+
}
|
|
401
|
+
.bar {
|
|
402
|
+
height: 8px;
|
|
403
|
+
border-radius: 99px;
|
|
404
|
+
background: color-mix(in srgb, var(--fg) 14%, var(--bg));
|
|
405
|
+
overflow: hidden;
|
|
406
|
+
margin-bottom: 18px;
|
|
407
|
+
}
|
|
408
|
+
#bar-fill {
|
|
409
|
+
height: 100%;
|
|
410
|
+
width: 0%;
|
|
411
|
+
background: var(--accent);
|
|
412
|
+
border-radius: 99px;
|
|
413
|
+
transition: width 0.3s ease;
|
|
414
|
+
}
|
|
415
|
+
#question { margin: 0 0 16px; font-size: 1.25rem; line-height: 1.4; }
|
|
416
|
+
#answers { display: grid; gap: 10px; }
|
|
417
|
+
.answer {
|
|
418
|
+
background: color-mix(in srgb, var(--fg) 14%, var(--bg));
|
|
419
|
+
color: var(--fg);
|
|
420
|
+
border: 2px solid transparent;
|
|
421
|
+
border-radius: 12px;
|
|
422
|
+
padding: 12px 16px;
|
|
423
|
+
font-size: 1rem;
|
|
424
|
+
font-weight: 600;
|
|
425
|
+
text-align: left;
|
|
426
|
+
cursor: pointer;
|
|
427
|
+
transition: transform 0.12s ease, border-color 0.12s ease;
|
|
428
|
+
}
|
|
429
|
+
.answer:hover:not(:disabled) { transform: translateY(-2px); border-color: var(--accent); }
|
|
430
|
+
.answer:focus-visible { outline: 3px solid var(--accent); }
|
|
431
|
+
.answer:disabled { cursor: default; opacity: 0.85; }
|
|
432
|
+
.answer.right { background: #2e9e6b; color: #ffffff; }
|
|
433
|
+
.answer.wrong { background: #c74f4f; color: #ffffff; animation: shake 0.3s ease; }
|
|
434
|
+
.answer.picked { border-color: var(--accent); transform: scale(0.98); }
|
|
435
|
+
#feedback { min-height: 2.4em; margin: 14px 0 4px; line-height: 1.45; }
|
|
436
|
+
.big {
|
|
437
|
+
background: var(--accent);
|
|
438
|
+
color: var(--bg);
|
|
439
|
+
border: none;
|
|
440
|
+
border-radius: 12px;
|
|
441
|
+
padding: 12px 22px;
|
|
442
|
+
font-size: 1rem;
|
|
443
|
+
font-weight: 700;
|
|
444
|
+
cursor: pointer;
|
|
445
|
+
transition: transform 0.12s ease;
|
|
446
|
+
}
|
|
447
|
+
.big:hover { transform: translateY(-2px); }
|
|
448
|
+
.big:focus-visible { outline: 3px solid var(--fg); }
|
|
449
|
+
.hidden { display: none; }
|
|
450
|
+
#end-emoji { font-size: 4rem; margin-bottom: 6px; }
|
|
451
|
+
#end-title { margin: 0 0 8px; }
|
|
452
|
+
#end-copy { margin: 0 0 18px; line-height: 1.5; }
|
|
453
|
+
.tip { opacity: 0.65; font-size: 0.85rem; margin-top: 16px; }
|
|
454
|
+
.confetti {
|
|
455
|
+
position: fixed;
|
|
456
|
+
top: -2rem;
|
|
457
|
+
font-size: 1.5rem;
|
|
458
|
+
pointer-events: none;
|
|
459
|
+
animation: fall 2.2s linear forwards;
|
|
460
|
+
}
|
|
461
|
+
@keyframes fall { to { transform: translateY(110vh) rotate(360deg); } }
|
|
462
|
+
@keyframes shake {
|
|
463
|
+
0% { transform: translateX(0); }
|
|
464
|
+
25% { transform: translateX(-5px); }
|
|
465
|
+
75% { transform: translateX(5px); }
|
|
466
|
+
100% { transform: translateX(0); }
|
|
467
|
+
}
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
function buildGameJs(theme, data) {
|
|
471
|
+
return `// Quiz Show: ${theme.label}. Made with Termi.
|
|
472
|
+
//
|
|
473
|
+
// HOW YOUR QUIZ WORKS:
|
|
474
|
+
// 1. QUIZ_TYPE picks the mode: "trivia" or "personality". Try flipping it!
|
|
475
|
+
// 2. Trivia questions live in TRIVIA_QUESTIONS. "correct" is the answer spot,
|
|
476
|
+
// counting from 0.
|
|
477
|
+
// 3. Personality questions live in PERSONALITY_QUESTIONS. Each answer points
|
|
478
|
+
// to a character in CHARACTERS.
|
|
479
|
+
// Save the file and refresh the page to see your changes.
|
|
480
|
+
|
|
481
|
+
const THEME = ${jsData({
|
|
482
|
+
id: theme.id,
|
|
483
|
+
label: theme.label,
|
|
484
|
+
emoji: theme.emoji,
|
|
485
|
+
palette: theme.palette,
|
|
486
|
+
glyphs: theme.glyphs,
|
|
487
|
+
strings: theme.strings,
|
|
488
|
+
})};
|
|
489
|
+
|
|
490
|
+
// Pick your quiz style: "trivia" or "personality".
|
|
491
|
+
const QUIZ_TYPE = ${JSON.stringify(data.quizType)};
|
|
492
|
+
|
|
493
|
+
// === YOUR TRIVIA QUESTIONS (edit this part!) ===
|
|
494
|
+
const TRIVIA_QUESTIONS = ${jsData(data.trivia)};
|
|
495
|
+
// === END OF TRIVIA QUESTIONS ===
|
|
496
|
+
|
|
497
|
+
// === YOUR PERSONALITY QUIZ (edit this part!) ===
|
|
498
|
+
const PERSONALITY_QUESTIONS = ${jsData(data.personality)};
|
|
499
|
+
const CHARACTERS = ${jsData(data.characters)};
|
|
500
|
+
// === END OF PERSONALITY QUIZ ===
|
|
501
|
+
|
|
502
|
+
// ----- The quiz engine starts here. Curious? Read on! -----
|
|
503
|
+
const progressEl = document.getElementById("progress");
|
|
504
|
+
const bestEl = document.getElementById("best");
|
|
505
|
+
const barFillEl = document.getElementById("bar-fill");
|
|
506
|
+
const questionEl = document.getElementById("question");
|
|
507
|
+
const answersEl = document.getElementById("answers");
|
|
508
|
+
const feedbackEl = document.getElementById("feedback");
|
|
509
|
+
const nextBtn = document.getElementById("next");
|
|
510
|
+
const quizEl = document.getElementById("quiz");
|
|
511
|
+
const endEl = document.getElementById("end");
|
|
512
|
+
const endEmojiEl = document.getElementById("end-emoji");
|
|
513
|
+
const endTitleEl = document.getElementById("end-title");
|
|
514
|
+
const endCopyEl = document.getElementById("end-copy");
|
|
515
|
+
const playAgainBtn = document.getElementById("play-again");
|
|
516
|
+
|
|
517
|
+
const mode = QUIZ_TYPE === "personality" ? "personality" : "trivia";
|
|
518
|
+
const questions = mode === "personality" ? PERSONALITY_QUESTIONS : TRIVIA_QUESTIONS;
|
|
519
|
+
const BEST_KEY = "termi-quiz-best-" + THEME.id;
|
|
520
|
+
let index = 0;
|
|
521
|
+
let score = 0;
|
|
522
|
+
let tally = {};
|
|
523
|
+
|
|
524
|
+
function loadBest() {
|
|
525
|
+
try {
|
|
526
|
+
return Number(localStorage.getItem(BEST_KEY) || "0");
|
|
527
|
+
} catch (err) {
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function saveBest(value) {
|
|
533
|
+
try {
|
|
534
|
+
localStorage.setItem(BEST_KEY, String(value));
|
|
535
|
+
} catch (err) {
|
|
536
|
+
// Storage is off. The quiz still plays fine.
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function updateBestNote() {
|
|
541
|
+
if (mode !== "trivia") {
|
|
542
|
+
bestEl.textContent = "";
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const best = loadBest();
|
|
546
|
+
bestEl.textContent = best > 0 ? THEME.strings.bestLabel + ": " + best : "";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function throwConfetti() {
|
|
550
|
+
const bits = [THEME.glyphs.spark, THEME.glyphs.star, THEME.glyphs.trophy];
|
|
551
|
+
for (let i = 0; i < 16; i += 1) {
|
|
552
|
+
const bit = document.createElement("span");
|
|
553
|
+
bit.className = "confetti";
|
|
554
|
+
bit.textContent = bits[i % bits.length];
|
|
555
|
+
bit.style.left = Math.floor(Math.random() * 100) + "%";
|
|
556
|
+
bit.style.animationDelay = (Math.random() * 0.6).toFixed(2) + "s";
|
|
557
|
+
document.body.appendChild(bit);
|
|
558
|
+
setTimeout(function () {
|
|
559
|
+
bit.remove();
|
|
560
|
+
}, 2600);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function renderQuestion() {
|
|
565
|
+
feedbackEl.textContent = "";
|
|
566
|
+
nextBtn.classList.add("hidden");
|
|
567
|
+
answersEl.textContent = "";
|
|
568
|
+
const q = questions[index];
|
|
569
|
+
progressEl.textContent = "Question " + (index + 1) + " of " + questions.length;
|
|
570
|
+
barFillEl.style.width = Math.round((index / questions.length) * 100) + "%";
|
|
571
|
+
questionEl.textContent = q.q;
|
|
572
|
+
q.answers.forEach(function (answer, i) {
|
|
573
|
+
const btn = document.createElement("button");
|
|
574
|
+
btn.className = "answer";
|
|
575
|
+
btn.type = "button";
|
|
576
|
+
btn.textContent = mode === "personality" ? answer.text : answer;
|
|
577
|
+
btn.addEventListener("click", function () {
|
|
578
|
+
if (mode === "personality") {
|
|
579
|
+
pickPersonality(btn, answer);
|
|
580
|
+
} else {
|
|
581
|
+
pickTrivia(btn, i, q);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
answersEl.appendChild(btn);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function pickPersonality(btn, answer) {
|
|
589
|
+
const buttons = answersEl.querySelectorAll("button");
|
|
590
|
+
buttons.forEach(function (b) {
|
|
591
|
+
b.disabled = true;
|
|
592
|
+
});
|
|
593
|
+
btn.classList.add("picked");
|
|
594
|
+
tally[answer.character] = (tally[answer.character] || 0) + 1;
|
|
595
|
+
setTimeout(nextStep, 350);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function pickTrivia(btn, i, q) {
|
|
599
|
+
const buttons = answersEl.querySelectorAll("button");
|
|
600
|
+
buttons.forEach(function (b, bIndex) {
|
|
601
|
+
b.disabled = true;
|
|
602
|
+
if (bIndex === q.correct) {
|
|
603
|
+
b.classList.add("right");
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
if (i === q.correct) {
|
|
607
|
+
score += 1;
|
|
608
|
+
const praises = [THEME.strings.praise1, THEME.strings.praise2, THEME.strings.praise3];
|
|
609
|
+
const praise = praises[Math.floor(Math.random() * praises.length)];
|
|
610
|
+
feedbackEl.textContent = praise + " " + q.fact;
|
|
611
|
+
} else {
|
|
612
|
+
btn.classList.add("wrong");
|
|
613
|
+
feedbackEl.textContent =
|
|
614
|
+
THEME.strings.wrongLead + " The answer is " + q.answers[q.correct] + ". " + q.fact;
|
|
615
|
+
}
|
|
616
|
+
nextBtn.textContent = index + 1 >= questions.length ? THEME.strings.finish : THEME.strings.next;
|
|
617
|
+
nextBtn.classList.remove("hidden");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function nextStep() {
|
|
621
|
+
index += 1;
|
|
622
|
+
if (index >= questions.length) {
|
|
623
|
+
showEnd();
|
|
624
|
+
} else {
|
|
625
|
+
renderQuestion();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function gradeCopy() {
|
|
630
|
+
const share = score / questions.length;
|
|
631
|
+
if (share >= 1) {
|
|
632
|
+
return "Perfect round! You are a quiz champion.";
|
|
633
|
+
}
|
|
634
|
+
if (share >= 0.75) {
|
|
635
|
+
return "Wow! You know so much.";
|
|
636
|
+
}
|
|
637
|
+
if (share >= 0.5) {
|
|
638
|
+
return "Nice work! One more round will make you even sharper.";
|
|
639
|
+
}
|
|
640
|
+
return "Good try! Every round teaches you something new.";
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function personalityWinner() {
|
|
644
|
+
let bestId = null;
|
|
645
|
+
let bestCount = -1;
|
|
646
|
+
Object.keys(CHARACTERS).forEach(function (id) {
|
|
647
|
+
const count = tally[id] || 0;
|
|
648
|
+
if (count > bestCount) {
|
|
649
|
+
bestCount = count;
|
|
650
|
+
bestId = id;
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
return CHARACTERS[bestId];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function showEnd() {
|
|
657
|
+
quizEl.classList.add("hidden");
|
|
658
|
+
endEl.classList.remove("hidden");
|
|
659
|
+
if (mode === "personality") {
|
|
660
|
+
const winner = personalityWinner();
|
|
661
|
+
endEmojiEl.textContent = winner.emoji;
|
|
662
|
+
endTitleEl.textContent = THEME.strings.resultLead + " " + winner.name + "!";
|
|
663
|
+
endCopyEl.textContent = winner.line;
|
|
664
|
+
} else {
|
|
665
|
+
endEmojiEl.textContent = THEME.glyphs.trophy;
|
|
666
|
+
endTitleEl.textContent = "You got " + score + " of " + questions.length + "!";
|
|
667
|
+
let copy = gradeCopy();
|
|
668
|
+
const best = loadBest();
|
|
669
|
+
if (score > best) {
|
|
670
|
+
saveBest(score);
|
|
671
|
+
if (best > 0) {
|
|
672
|
+
copy += " New personal best!";
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
endCopyEl.textContent = copy;
|
|
676
|
+
}
|
|
677
|
+
throwConfetti();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function restart() {
|
|
681
|
+
index = 0;
|
|
682
|
+
score = 0;
|
|
683
|
+
tally = {};
|
|
684
|
+
endEl.classList.add("hidden");
|
|
685
|
+
quizEl.classList.remove("hidden");
|
|
686
|
+
updateBestNote();
|
|
687
|
+
renderQuestion();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
nextBtn.addEventListener("click", nextStep);
|
|
691
|
+
playAgainBtn.addEventListener("click", restart);
|
|
692
|
+
|
|
693
|
+
updateBestNote();
|
|
694
|
+
renderQuestion();
|
|
695
|
+
`;
|
|
696
|
+
}
|
|
697
|
+
function buildTermiMd(theme, prettyName, data) {
|
|
698
|
+
const modeLine = data.quizType === 'personality'
|
|
699
|
+
? `A personality quiz with ${data.personality.length} questions and ${Object.keys(data.characters).length} characters.`
|
|
700
|
+
: `A trivia quiz with ${data.trivia.length} questions, scoring, and kind feedback.`;
|
|
701
|
+
return [
|
|
702
|
+
`# ${prettyName}`,
|
|
703
|
+
'',
|
|
704
|
+
'## What this is',
|
|
705
|
+
`${modeLine} Flip QUIZ_TYPE in game.js to switch modes.`,
|
|
706
|
+
'',
|
|
707
|
+
'## Files',
|
|
708
|
+
'- index.html: the page and its parts.',
|
|
709
|
+
'- style.css: the colors and look.',
|
|
710
|
+
'- game.js: the question lists at the top, then the quiz engine.',
|
|
711
|
+
'',
|
|
712
|
+
'## Built so far',
|
|
713
|
+
`- Starter ${theme.label} quiz with both trivia and personality data ready.`,
|
|
714
|
+
'',
|
|
715
|
+
'## Recap line',
|
|
716
|
+
`We built a ${theme.label} quiz that is ready to play.`,
|
|
717
|
+
'',
|
|
718
|
+
].join('\n');
|
|
719
|
+
}
|
|
720
|
+
function dataFor(theme) {
|
|
721
|
+
return theme.id === 'which-character' ? characterData : animalsData;
|
|
722
|
+
}
|
|
723
|
+
export const quizzesScaffold = {
|
|
724
|
+
id: 'quizzes',
|
|
725
|
+
label: 'Quiz Show',
|
|
726
|
+
emoji: '\u{2753}',
|
|
727
|
+
ageNote: 'Great for ages 9 and up. Make quizzes for friends and family.',
|
|
728
|
+
themes: [animalsTheme, characterTheme],
|
|
729
|
+
files(theme, prettyName) {
|
|
730
|
+
const data = dataFor(theme);
|
|
731
|
+
return {
|
|
732
|
+
'index.html': buildHtml(theme, prettyName),
|
|
733
|
+
'style.css': buildCss(theme),
|
|
734
|
+
'game.js': buildGameJs(theme, data),
|
|
735
|
+
'TERMI.md': buildTermiMd(theme, prettyName, data),
|
|
736
|
+
};
|
|
737
|
+
},
|
|
738
|
+
starterPrompts(theme) {
|
|
739
|
+
if (theme.id === 'which-character') {
|
|
740
|
+
return [
|
|
741
|
+
'Add a new character called the dragon tamer',
|
|
742
|
+
'Write three more questions about snacks and weekends',
|
|
743
|
+
'Give each result card its own colors',
|
|
744
|
+
'Add more confetti when I get my result',
|
|
745
|
+
'Switch it to trivia mode with space questions',
|
|
746
|
+
];
|
|
747
|
+
}
|
|
748
|
+
return [
|
|
749
|
+
'Add five more animal questions',
|
|
750
|
+
'Make a question about sharks with a cool fact',
|
|
751
|
+
'Add a timer so each question feels exciting',
|
|
752
|
+
'Show a silly face when I get one wrong',
|
|
753
|
+
'Switch it to a which animal are you quiz',
|
|
754
|
+
];
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
export default quizzesScaffold;
|