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,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Talking Character: a scripted-dialog character builder.
|
|
3
|
+
* NO AI at runtime and no network. The kid edits CHARACTER and DIALOG
|
|
4
|
+
* at the top of game.js. A chat-style UI matches typed words against
|
|
5
|
+
* keywords (case-insensitive). Unknown words rotate through fallbacks.
|
|
6
|
+
* Framing: a game character you BUILD, never a friend with feelings.
|
|
7
|
+
*/
|
|
8
|
+
function jsData(value) {
|
|
9
|
+
return JSON.stringify(value, null, 2);
|
|
10
|
+
}
|
|
11
|
+
function escapeHtml(text) {
|
|
12
|
+
return text
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
16
|
+
.replace(/"/g, '"');
|
|
17
|
+
}
|
|
18
|
+
const quizHostTheme = {
|
|
19
|
+
id: 'quiz-host',
|
|
20
|
+
label: 'Quiz Host',
|
|
21
|
+
emoji: '\u{1F3A4}',
|
|
22
|
+
palette: { bg: '#2a1158', fg: '#fff7fb', accent: '#ffd23f' },
|
|
23
|
+
glyphs: { spark: '\u{2728}', mic: '\u{1F3A4}' },
|
|
24
|
+
strings: {
|
|
25
|
+
tagline: 'A game show host that runs on your script.',
|
|
26
|
+
chip1: 'hello',
|
|
27
|
+
chip2: 'quiz',
|
|
28
|
+
chip3: 'joke',
|
|
29
|
+
inputPlaceholder: 'Type a message to your host',
|
|
30
|
+
sendLabel: 'Send',
|
|
31
|
+
},
|
|
32
|
+
narrativeIntro: 'Lights on! You are building a game show host. You write every line it says.',
|
|
33
|
+
nonViolent: true,
|
|
34
|
+
nonCompetitive: true,
|
|
35
|
+
};
|
|
36
|
+
const robotTheme = {
|
|
37
|
+
id: 'sidekick-robot',
|
|
38
|
+
label: 'Sidekick Robot',
|
|
39
|
+
emoji: '\u{1F916}',
|
|
40
|
+
palette: { bg: '#101a2e', fg: '#eaf6ff', accent: '#59d2fe' },
|
|
41
|
+
glyphs: { spark: '\u{26A1}', mic: '\u{1F50B}' },
|
|
42
|
+
strings: {
|
|
43
|
+
tagline: 'A robot sidekick that runs on your script.',
|
|
44
|
+
chip1: 'hello',
|
|
45
|
+
chip2: 'mission',
|
|
46
|
+
chip3: 'dance',
|
|
47
|
+
inputPlaceholder: 'Type a message to your robot',
|
|
48
|
+
sendLabel: 'Send',
|
|
49
|
+
},
|
|
50
|
+
narrativeIntro: 'Beep! You are building a robot sidekick. You write every line it says.',
|
|
51
|
+
nonViolent: true,
|
|
52
|
+
nonCompetitive: true,
|
|
53
|
+
};
|
|
54
|
+
const quizHostData = {
|
|
55
|
+
character: {
|
|
56
|
+
name: 'Quizmo',
|
|
57
|
+
role: 'game show host',
|
|
58
|
+
greeting: 'Welcome to the show! I am Quizmo, your quiz host. Type hello, quiz, or joke to start!',
|
|
59
|
+
catchphrases: ['No way to lose on this show!', 'Ding ding ding!', 'That answer sparkles!'],
|
|
60
|
+
moods: {
|
|
61
|
+
happy: '\u{1F604}',
|
|
62
|
+
excited: '\u{1F929}',
|
|
63
|
+
thinking: '\u{1F914}',
|
|
64
|
+
silly: '\u{1F61C}',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
dialog: [
|
|
68
|
+
{
|
|
69
|
+
keywords: ['hello', 'hi', 'hey', 'howdy'],
|
|
70
|
+
reply: 'Hello, superstar! Welcome to the brightest show on this computer!',
|
|
71
|
+
mood: 'happy',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
keywords: ['quiz', 'question', 'trivia'],
|
|
75
|
+
reply: 'Lightning round! Which animal has three hearts? Type octopus if you know it!',
|
|
76
|
+
mood: 'excited',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
keywords: ['octopus'],
|
|
80
|
+
reply: 'Ding ding ding! Correct! An octopus has three hearts. Take a bow!',
|
|
81
|
+
mood: 'excited',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
keywords: ['joke', 'funny'],
|
|
85
|
+
reply: 'Why did the math book look sad? It had too many problems!',
|
|
86
|
+
mood: 'silly',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
keywords: ['name', 'who are you'],
|
|
90
|
+
reply: 'I am Quizmo! Part host, part confetti cannon, all questions.',
|
|
91
|
+
mood: 'happy',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
keywords: ['how are you'],
|
|
95
|
+
reply: 'Feeling shiny! A host is only as good as the next question.',
|
|
96
|
+
mood: 'happy',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
keywords: ['prize', 'win'],
|
|
100
|
+
reply: 'Tonight, the grand prize is bragging rights and a drum roll. Drrrrum!',
|
|
101
|
+
mood: 'excited',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
keywords: ['score', 'points'],
|
|
105
|
+
reply: 'The scoreboard says: you have one million style points.',
|
|
106
|
+
mood: 'silly',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
keywords: ['cheat'],
|
|
110
|
+
reply: 'No cheating on my show! The best players use their brains.',
|
|
111
|
+
mood: 'thinking',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
keywords: ['secret'],
|
|
115
|
+
reply: 'A good host never spoils the surprise. Ask me for a quiz instead!',
|
|
116
|
+
mood: 'thinking',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
keywords: ['music', 'song'],
|
|
120
|
+
reply: 'Cue the theme music! Da da da DAAA! You hum the rest.',
|
|
121
|
+
mood: 'silly',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
keywords: ['help'],
|
|
125
|
+
reply: 'Try typing: quiz, joke, prize, or secret. I have a line for each!',
|
|
126
|
+
mood: 'thinking',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
keywords: ['love you', 'best friend'],
|
|
130
|
+
reply: 'Aw, thanks! Remember, I am a character you built from a script. You are the real star here.',
|
|
131
|
+
mood: 'happy',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
keywords: ['bye', 'goodbye', 'goodnight'],
|
|
135
|
+
reply: 'That is our show! You have been a fantastic player. See you next round!',
|
|
136
|
+
mood: 'happy',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
fallbacks: [
|
|
140
|
+
'Ooh, a mystery word! Teach me a reply for it in game.js.',
|
|
141
|
+
'My cue cards are blank for that one. Add it to my script!',
|
|
142
|
+
'Hmm, that is not in my script yet. You write my lines, builder!',
|
|
143
|
+
'Plot twist! I do not know that word. Yet.',
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
const robotData = {
|
|
147
|
+
character: {
|
|
148
|
+
name: 'Bolt',
|
|
149
|
+
role: 'sidekick robot',
|
|
150
|
+
greeting: 'Beep! Bolt online. Sidekick mode ready. Type hello, mission, or joke!',
|
|
151
|
+
catchphrases: ['Beep beep!', 'Sidekick mode: ON.', 'Calculating awesome levels.'],
|
|
152
|
+
moods: {
|
|
153
|
+
happy: '\u{1F916}',
|
|
154
|
+
excited: '\u{26A1}',
|
|
155
|
+
thinking: '\u{1F50D}',
|
|
156
|
+
silly: '\u{1F643}',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
dialog: [
|
|
160
|
+
{
|
|
161
|
+
keywords: ['hello', 'hi', 'hey'],
|
|
162
|
+
reply: 'Beep! Hello, hero. Bolt reporting for duty!',
|
|
163
|
+
mood: 'happy',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
keywords: ['mission', 'quest', 'adventure'],
|
|
167
|
+
reply: 'Mission found: rescue the snacks from the kitchen. Danger level: crumbs.',
|
|
168
|
+
mood: 'excited',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
keywords: ['joke', 'funny'],
|
|
172
|
+
reply: 'Why did the robot go on vacation? To recharge its batteries!',
|
|
173
|
+
mood: 'silly',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
keywords: ['beep', 'boop'],
|
|
177
|
+
reply: 'Boop! You speak robot? We are going to be a great team.',
|
|
178
|
+
mood: 'excited',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
keywords: ['snack', 'food', 'hungry'],
|
|
182
|
+
reply: 'I eat tiny bolts and screws. Crunchy! What is your favorite snack?',
|
|
183
|
+
mood: 'silly',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
keywords: ['dance'],
|
|
187
|
+
reply: 'Activating dance mode. Beep boop, beep boop, robot wiggle!',
|
|
188
|
+
mood: 'excited',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
keywords: ['name', 'who are you'],
|
|
192
|
+
reply: 'I am Bolt, your sidekick robot. Built by you, powered by your script.',
|
|
193
|
+
mood: 'happy',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
keywords: ['how are you'],
|
|
197
|
+
reply: 'Systems green! Battery at 98 percent. Adventure levels rising.',
|
|
198
|
+
mood: 'happy',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
keywords: ['help'],
|
|
202
|
+
reply: 'Try typing: mission, joke, dance, beep, or villain. I know those!',
|
|
203
|
+
mood: 'thinking',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
keywords: ['villain', 'bad guy'],
|
|
207
|
+
reply: 'Villain alert! Our plan: kindness, clever traps, and very loud alarms.',
|
|
208
|
+
mood: 'thinking',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
keywords: ['power', 'powers', 'super power'],
|
|
212
|
+
reply: 'My powers: magnet fingers, night vision, and excellent high fives.',
|
|
213
|
+
mood: 'excited',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
keywords: ['sleep', 'tired'],
|
|
217
|
+
reply: 'Robots do not sleep. We update. Current update: 12 percent more jokes.',
|
|
218
|
+
mood: 'silly',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
keywords: ['love you', 'best friend'],
|
|
222
|
+
reply: 'Beep! I am a robot character you built. No feelings chip installed. But your script makes me extra cool.',
|
|
223
|
+
mood: 'happy',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
keywords: ['bye', 'goodbye'],
|
|
227
|
+
reply: 'Powering down. Beep! Wake me for the next mission, hero.',
|
|
228
|
+
mood: 'happy',
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
fallbacks: [
|
|
232
|
+
'Error 404: reply not found. Add one to my script in game.js!',
|
|
233
|
+
'Beep? That word is not in my data banks yet.',
|
|
234
|
+
'My circuits are stumped. Teach me that one, builder!',
|
|
235
|
+
'Unknown input! You can write my reply for that in game.js.',
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
function buildHtml(theme, prettyName) {
|
|
239
|
+
const s = theme.strings;
|
|
240
|
+
return `<!DOCTYPE html>
|
|
241
|
+
<html lang="en">
|
|
242
|
+
<head>
|
|
243
|
+
<meta charset="UTF-8">
|
|
244
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
245
|
+
<title>${escapeHtml(prettyName)}</title>
|
|
246
|
+
<link rel="icon" href="data:,">
|
|
247
|
+
<link rel="stylesheet" href="style.css">
|
|
248
|
+
<script src="game.js" defer></script>
|
|
249
|
+
</head>
|
|
250
|
+
<body>
|
|
251
|
+
<main class="frame">
|
|
252
|
+
<header>
|
|
253
|
+
<div id="avatar">${theme.emoji}</div>
|
|
254
|
+
<div>
|
|
255
|
+
<h1 id="char-name">${escapeHtml(prettyName)}</h1>
|
|
256
|
+
<p class="tagline">${s['tagline'] ?? ''}</p>
|
|
257
|
+
</div>
|
|
258
|
+
</header>
|
|
259
|
+
<section id="chat-log" class="card log" aria-live="polite"></section>
|
|
260
|
+
<div id="chips"></div>
|
|
261
|
+
<form id="chat-form">
|
|
262
|
+
<input id="chat-input" type="text" autocomplete="off" maxlength="200"
|
|
263
|
+
placeholder="${s['inputPlaceholder'] ?? 'Type a message'}" aria-label="Your message">
|
|
264
|
+
<button class="big" type="submit">${s['sendLabel'] ?? 'Send'}</button>
|
|
265
|
+
</form>
|
|
266
|
+
<p id="disclosure" class="tip"></p>
|
|
267
|
+
</main>
|
|
268
|
+
</body>
|
|
269
|
+
</html>
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
function buildCss(theme) {
|
|
273
|
+
const p = theme.palette;
|
|
274
|
+
return `/* Talking Character styles. Colors come from your THEME in game.js. */
|
|
275
|
+
* { box-sizing: border-box; }
|
|
276
|
+
:root {
|
|
277
|
+
--bg: ${p.bg};
|
|
278
|
+
--fg: ${p.fg};
|
|
279
|
+
--accent: ${p.accent};
|
|
280
|
+
}
|
|
281
|
+
body {
|
|
282
|
+
margin: 0;
|
|
283
|
+
min-height: 100vh;
|
|
284
|
+
background: var(--bg);
|
|
285
|
+
background-image: radial-gradient(circle at 30% 0%, color-mix(in srgb, var(--accent) 16%, var(--bg)), var(--bg) 60%);
|
|
286
|
+
color: var(--fg);
|
|
287
|
+
font-family: system-ui, "Segoe UI", Roboto, Arial, sans-serif;
|
|
288
|
+
display: flex;
|
|
289
|
+
justify-content: center;
|
|
290
|
+
padding: 28px 16px;
|
|
291
|
+
}
|
|
292
|
+
.frame { width: min(560px, 100%); }
|
|
293
|
+
header {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
gap: 14px;
|
|
297
|
+
margin-bottom: 14px;
|
|
298
|
+
}
|
|
299
|
+
#avatar {
|
|
300
|
+
font-size: 3rem;
|
|
301
|
+
line-height: 1;
|
|
302
|
+
background: color-mix(in srgb, var(--accent) 24%, var(--bg));
|
|
303
|
+
border-radius: 50%;
|
|
304
|
+
width: 72px;
|
|
305
|
+
height: 72px;
|
|
306
|
+
display: flex;
|
|
307
|
+
align-items: center;
|
|
308
|
+
justify-content: center;
|
|
309
|
+
flex-shrink: 0;
|
|
310
|
+
}
|
|
311
|
+
header h1 { margin: 0; font-size: 1.5rem; }
|
|
312
|
+
.tagline { margin: 2px 0 0; opacity: 0.8; font-size: 0.92rem; }
|
|
313
|
+
.card {
|
|
314
|
+
background: rgba(255, 255, 255, 0.07);
|
|
315
|
+
background: color-mix(in srgb, var(--fg) 8%, var(--bg));
|
|
316
|
+
border: 1px solid color-mix(in srgb, var(--fg) 16%, var(--bg));
|
|
317
|
+
border-radius: 16px;
|
|
318
|
+
}
|
|
319
|
+
.log {
|
|
320
|
+
height: 340px;
|
|
321
|
+
overflow-y: auto;
|
|
322
|
+
padding: 16px;
|
|
323
|
+
display: flex;
|
|
324
|
+
flex-direction: column;
|
|
325
|
+
gap: 10px;
|
|
326
|
+
}
|
|
327
|
+
.bubble {
|
|
328
|
+
max-width: 82%;
|
|
329
|
+
padding: 10px 14px;
|
|
330
|
+
border-radius: 16px;
|
|
331
|
+
line-height: 1.45;
|
|
332
|
+
animation: pop 0.18s ease-out;
|
|
333
|
+
}
|
|
334
|
+
.bubble.kid {
|
|
335
|
+
align-self: flex-end;
|
|
336
|
+
background: var(--accent);
|
|
337
|
+
color: var(--bg);
|
|
338
|
+
border-bottom-right-radius: 4px;
|
|
339
|
+
font-weight: 600;
|
|
340
|
+
}
|
|
341
|
+
.bubble.bot {
|
|
342
|
+
align-self: flex-start;
|
|
343
|
+
background: color-mix(in srgb, var(--fg) 14%, var(--bg));
|
|
344
|
+
border-bottom-left-radius: 4px;
|
|
345
|
+
}
|
|
346
|
+
.bubble.typing { opacity: 0.6; letter-spacing: 2px; }
|
|
347
|
+
#chips { display: flex; gap: 8px; margin: 12px 0; flex-wrap: wrap; }
|
|
348
|
+
.chip {
|
|
349
|
+
background: transparent;
|
|
350
|
+
color: var(--fg);
|
|
351
|
+
border: 1px solid color-mix(in srgb, var(--fg) 35%, var(--bg));
|
|
352
|
+
border-radius: 99px;
|
|
353
|
+
padding: 6px 14px;
|
|
354
|
+
font-size: 0.9rem;
|
|
355
|
+
cursor: pointer;
|
|
356
|
+
}
|
|
357
|
+
.chip:hover { border-color: var(--accent); color: var(--accent); }
|
|
358
|
+
#chat-form { display: flex; gap: 8px; }
|
|
359
|
+
#chat-input {
|
|
360
|
+
flex: 1;
|
|
361
|
+
background: color-mix(in srgb, var(--fg) 8%, var(--bg));
|
|
362
|
+
color: var(--fg);
|
|
363
|
+
border: 1px solid color-mix(in srgb, var(--fg) 20%, var(--bg));
|
|
364
|
+
border-radius: 12px;
|
|
365
|
+
padding: 12px 14px;
|
|
366
|
+
font-size: 1rem;
|
|
367
|
+
}
|
|
368
|
+
#chat-input:focus { outline: 2px solid var(--accent); }
|
|
369
|
+
.big {
|
|
370
|
+
background: var(--accent);
|
|
371
|
+
color: var(--bg);
|
|
372
|
+
border: none;
|
|
373
|
+
border-radius: 12px;
|
|
374
|
+
padding: 12px 20px;
|
|
375
|
+
font-size: 1rem;
|
|
376
|
+
font-weight: 700;
|
|
377
|
+
cursor: pointer;
|
|
378
|
+
transition: transform 0.12s ease;
|
|
379
|
+
}
|
|
380
|
+
.big:hover { transform: translateY(-2px); }
|
|
381
|
+
.big:focus-visible { outline: 3px solid var(--fg); }
|
|
382
|
+
.tip { opacity: 0.65; font-size: 0.85rem; margin-top: 14px; }
|
|
383
|
+
@keyframes pop { from { transform: scale(0.92); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
function buildGameJs(theme, data) {
|
|
387
|
+
return `// Talking Character: ${theme.label}. Made with Termi.
|
|
388
|
+
//
|
|
389
|
+
// HOW YOUR CHARACTER WORKS:
|
|
390
|
+
// 1. CHARACTER holds the name, greeting, catchphrases, and mood faces.
|
|
391
|
+
// 2. DIALOG is the script. If a message has a keyword, you get that reply.
|
|
392
|
+
// 3. FALLBACKS are used when no keyword matches. They take turns.
|
|
393
|
+
// Keywords should be lowercase words. Add as many lines as you want!
|
|
394
|
+
// This is a character you built. It only says lines from your script.
|
|
395
|
+
|
|
396
|
+
const THEME = ${jsData({
|
|
397
|
+
id: theme.id,
|
|
398
|
+
label: theme.label,
|
|
399
|
+
emoji: theme.emoji,
|
|
400
|
+
palette: theme.palette,
|
|
401
|
+
glyphs: theme.glyphs,
|
|
402
|
+
strings: theme.strings,
|
|
403
|
+
})};
|
|
404
|
+
|
|
405
|
+
// === YOUR CHARACTER (edit this part!) ===
|
|
406
|
+
const CHARACTER = ${jsData(data.character)};
|
|
407
|
+
const DIALOG = ${jsData(data.dialog)};
|
|
408
|
+
const FALLBACKS = ${jsData(data.fallbacks)};
|
|
409
|
+
// === END OF YOUR CHARACTER ===
|
|
410
|
+
|
|
411
|
+
// ----- The chat engine starts here. Curious? Read on! -----
|
|
412
|
+
const avatarEl = document.getElementById("avatar");
|
|
413
|
+
const nameEl = document.getElementById("char-name");
|
|
414
|
+
const logEl = document.getElementById("chat-log");
|
|
415
|
+
const chipsEl = document.getElementById("chips");
|
|
416
|
+
const formEl = document.getElementById("chat-form");
|
|
417
|
+
const inputEl = document.getElementById("chat-input");
|
|
418
|
+
const disclosureEl = document.getElementById("disclosure");
|
|
419
|
+
|
|
420
|
+
let fallbackIndex = 0;
|
|
421
|
+
|
|
422
|
+
function addBubble(text, kind) {
|
|
423
|
+
const bubble = document.createElement("div");
|
|
424
|
+
bubble.className = "bubble " + kind;
|
|
425
|
+
bubble.textContent = text;
|
|
426
|
+
logEl.appendChild(bubble);
|
|
427
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
428
|
+
return bubble;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function setMood(mood) {
|
|
432
|
+
const face = CHARACTER.moods[mood];
|
|
433
|
+
if (face) {
|
|
434
|
+
avatarEl.textContent = face;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function normalize(text) {
|
|
439
|
+
const clean = text
|
|
440
|
+
.toLowerCase()
|
|
441
|
+
.replace(/[^a-z0-9 ]/g, " ")
|
|
442
|
+
.replace(/ +/g, " ")
|
|
443
|
+
.trim();
|
|
444
|
+
return " " + clean + " ";
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function findReply(message) {
|
|
448
|
+
const padded = normalize(message);
|
|
449
|
+
for (let i = 0; i < DIALOG.length; i += 1) {
|
|
450
|
+
const entry = DIALOG[i];
|
|
451
|
+
for (let k = 0; k < entry.keywords.length; k += 1) {
|
|
452
|
+
if (padded.indexOf(" " + entry.keywords[k] + " ") !== -1) {
|
|
453
|
+
return entry;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function respond(message) {
|
|
461
|
+
const typing = addBubble("...", "bot typing");
|
|
462
|
+
setTimeout(function () {
|
|
463
|
+
typing.remove();
|
|
464
|
+
const entry = findReply(message);
|
|
465
|
+
if (entry) {
|
|
466
|
+
if (entry.mood) {
|
|
467
|
+
setMood(entry.mood);
|
|
468
|
+
}
|
|
469
|
+
addBubble(entry.reply, "bot");
|
|
470
|
+
if (Math.random() < 0.25 && CHARACTER.catchphrases.length > 0) {
|
|
471
|
+
const pick = Math.floor(Math.random() * CHARACTER.catchphrases.length);
|
|
472
|
+
setTimeout(function () {
|
|
473
|
+
addBubble(CHARACTER.catchphrases[pick], "bot");
|
|
474
|
+
}, 420);
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
setMood("thinking");
|
|
478
|
+
addBubble(FALLBACKS[fallbackIndex % FALLBACKS.length], "bot");
|
|
479
|
+
fallbackIndex += 1;
|
|
480
|
+
}
|
|
481
|
+
}, 450);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function sendMessage(message) {
|
|
485
|
+
const trimmed = message.trim();
|
|
486
|
+
if (!trimmed) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
addBubble(trimmed, "kid");
|
|
490
|
+
respond(trimmed);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function buildChips() {
|
|
494
|
+
const chips = [THEME.strings.chip1, THEME.strings.chip2, THEME.strings.chip3];
|
|
495
|
+
chips.forEach(function (text) {
|
|
496
|
+
const btn = document.createElement("button");
|
|
497
|
+
btn.className = "chip";
|
|
498
|
+
btn.type = "button";
|
|
499
|
+
btn.textContent = "Try: " + text;
|
|
500
|
+
btn.addEventListener("click", function () {
|
|
501
|
+
sendMessage(text);
|
|
502
|
+
});
|
|
503
|
+
chipsEl.appendChild(btn);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
formEl.addEventListener("submit", function (event) {
|
|
508
|
+
event.preventDefault();
|
|
509
|
+
const message = inputEl.value;
|
|
510
|
+
inputEl.value = "";
|
|
511
|
+
sendMessage(message);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
nameEl.textContent = CHARACTER.name;
|
|
515
|
+
disclosureEl.textContent =
|
|
516
|
+
CHARACTER.name + " is a " + CHARACTER.role + " you built. Every line comes from your script in game.js.";
|
|
517
|
+
buildChips();
|
|
518
|
+
setMood("happy");
|
|
519
|
+
addBubble(CHARACTER.greeting, "bot");
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
function buildTermiMd(theme, prettyName, data) {
|
|
523
|
+
const lineCount = data.dialog.length + data.fallbacks.length;
|
|
524
|
+
return [
|
|
525
|
+
`# ${prettyName}`,
|
|
526
|
+
'',
|
|
527
|
+
'## What this is',
|
|
528
|
+
`A ${theme.label.toLowerCase()} character you script yourself. No AI runs here. It has ${lineCount} starter lines.`,
|
|
529
|
+
'',
|
|
530
|
+
'## Files',
|
|
531
|
+
'- index.html: the chat page and its parts.',
|
|
532
|
+
'- style.css: the colors and look.',
|
|
533
|
+
'- game.js: CHARACTER and DIALOG at the top, then the chat engine.',
|
|
534
|
+
'',
|
|
535
|
+
'## Built so far',
|
|
536
|
+
`- Starter ${data.character.name} character with ${data.dialog.length} scripted replies and ${data.fallbacks.length} fallbacks.`,
|
|
537
|
+
'',
|
|
538
|
+
'## Recap line',
|
|
539
|
+
`We built ${data.character.name}, a ${theme.label.toLowerCase()} that speaks from your script.`,
|
|
540
|
+
'',
|
|
541
|
+
].join('\n');
|
|
542
|
+
}
|
|
543
|
+
function dataFor(theme) {
|
|
544
|
+
return theme.id === 'sidekick-robot' ? robotData : quizHostData;
|
|
545
|
+
}
|
|
546
|
+
export const charactersScaffold = {
|
|
547
|
+
id: 'characters',
|
|
548
|
+
label: 'Talking Character',
|
|
549
|
+
emoji: '\u{1F916}',
|
|
550
|
+
ageNote: 'Great for ages 9 and up. You write every line your character can say.',
|
|
551
|
+
themes: [quizHostTheme, robotTheme],
|
|
552
|
+
files(theme, prettyName) {
|
|
553
|
+
const data = dataFor(theme);
|
|
554
|
+
return {
|
|
555
|
+
'index.html': buildHtml(theme, prettyName),
|
|
556
|
+
'style.css': buildCss(theme),
|
|
557
|
+
'game.js': buildGameJs(theme, data),
|
|
558
|
+
'TERMI.md': buildTermiMd(theme, prettyName, data),
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
starterPrompts(theme) {
|
|
562
|
+
if (theme.id === 'sidekick-robot') {
|
|
563
|
+
return [
|
|
564
|
+
'Teach Bolt a reply about dinosaurs',
|
|
565
|
+
'Add a low battery mood with sleepy answers',
|
|
566
|
+
'Make Bolt beep before every reply',
|
|
567
|
+
'Add a button that makes Bolt do a dance',
|
|
568
|
+
'Give Bolt three more jokes',
|
|
569
|
+
];
|
|
570
|
+
}
|
|
571
|
+
return [
|
|
572
|
+
'Give Quizmo five more trivia questions and answers',
|
|
573
|
+
'Add a reply for when I type pizza',
|
|
574
|
+
'Give Quizmo a grumpy mood with a new face',
|
|
575
|
+
'Make Quizmo tell knock knock jokes',
|
|
576
|
+
'Add a drum roll line before each answer',
|
|
577
|
+
];
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
export default charactersScaffold;
|