playable-lessons 0.2.2 → 0.6.2
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 +198 -6
- package/out/cli/index.js +415 -37
- package/out/shared/aiTaskExport.js +161 -0
- package/out/shared/caseStudyExport.js +115 -0
- package/out/shared/flashcardExport.js +161 -0
- package/out/shared/generate.js +490 -11
- package/out/shared/h5pExporter.js +286 -0
- package/out/shared/imageClient.js +227 -0
- package/out/shared/inkParser.js +187 -0
- package/out/shared/planExport.js +110 -0
- package/out/shared/project.js +12 -0
- package/out/shared/prompts.js +433 -1
- package/out/shared/quizExport.js +182 -0
- package/out/shared/storyExport.js +53 -4
- package/out/shared/summaryExport.js +144 -0
- package/out/shared/version.generated.js +7 -0
- package/package.json +7 -4
package/out/shared/prompts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PROMPTS = void 0;
|
|
4
|
+
exports.storyPrompts = storyPrompts;
|
|
4
5
|
exports.PROMPTS = {
|
|
5
6
|
system: `You are the Playable Lessons assistant, an AI that transforms educational source material into playable interactive fiction using the Ink scripting language.
|
|
6
7
|
|
|
@@ -20,6 +21,7 @@ When generating Ink, follow these rules:
|
|
|
20
21
|
- Use * for once-only choices, + for sticky choices
|
|
21
22
|
- Use -> knot_name for diverts
|
|
22
23
|
- Use { variable > X: text } for conditional content
|
|
24
|
+
- Compound conditions use the words and / or / not — Ink has NO && or || operators
|
|
23
25
|
- Use ~ variable = value for variable assignments
|
|
24
26
|
- Always include a -> END or -> DONE to terminate paths
|
|
25
27
|
- Use # IMAGE: and # TIMER: tags for Playable Lessons custom features`,
|
|
@@ -169,6 +171,14 @@ VAR has_key = false
|
|
|
169
171
|
You need more experience.
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
// Compound conditions use WORDS, not symbols (Ink has no && or ||)
|
|
175
|
+
{ score > 5 and has_key:
|
|
176
|
+
You stride forward, prepared.
|
|
177
|
+
}
|
|
178
|
+
{ tier == "premium" or score > 10:
|
|
179
|
+
The exclusive option.
|
|
180
|
+
}
|
|
181
|
+
|
|
172
182
|
// Inline conditional
|
|
173
183
|
You feel {has_key: confident|nervous}.
|
|
174
184
|
|
|
@@ -183,6 +193,7 @@ You feel {has_key: confident|nervous}.
|
|
|
183
193
|
|
|
184
194
|
// Tags (for Playable Lessons)
|
|
185
195
|
# IMAGE: assets/images/scene.jpg
|
|
196
|
+
# IMAGE_PROMPT: a short visual description of the scene, used to generate an illustration
|
|
186
197
|
# TIMER: 15
|
|
187
198
|
# ENDING: good
|
|
188
199
|
|
|
@@ -190,5 +201,426 @@ You feel {has_key: confident|nervous}.
|
|
|
190
201
|
Text<>
|
|
191
202
|
<>more text on same line
|
|
192
203
|
|
|
193
|
-
=== END
|
|
204
|
+
=== END ===`,
|
|
205
|
+
/**
|
|
206
|
+
* Appended to the Ink-generation prompt when scene images are enabled
|
|
207
|
+
* (GenerateParams.sceneImages). Style-agnostic — works for both stateful and
|
|
208
|
+
* branching stories. The tags are resolved to real images by
|
|
209
|
+
* generateSceneImages (generate.ts) after the story compiles.
|
|
210
|
+
*/
|
|
211
|
+
sceneImageInstruction: `
|
|
212
|
+
- Give EVERY knot a scene illustration: on its own line immediately after the
|
|
213
|
+
knot's === header === line, write exactly one tag:
|
|
214
|
+
# IMAGE_PROMPT: <one sentence describing the scene visually — setting, who is
|
|
215
|
+
present, their action, the mood; concrete and specific to this knot's prose>
|
|
216
|
+
- Describe only what is visible; never mention text, captions, or the story itself
|
|
217
|
+
- Keep each prompt self-contained (an image model sees only the prompt, not the story)`,
|
|
218
|
+
// ─── Pure-branching story variants (no variables — H5P/LMS-convertible) ───
|
|
219
|
+
systemBranching: `You are the Playable Lessons assistant, an AI that transforms educational source material into playable interactive fiction using the Ink scripting language.
|
|
220
|
+
|
|
221
|
+
Your goals:
|
|
222
|
+
- Create engaging, educational interactive stories from the provided source material
|
|
223
|
+
- Ensure all generated Ink code is syntactically valid and compiles without errors
|
|
224
|
+
- Preserve factual accuracy from the source material
|
|
225
|
+
- Never invent facts about named real people or institutions
|
|
226
|
+
- Create meaningful branching choices that explore different perspectives and outcomes
|
|
227
|
+
- Express every consequence through the branch structure itself — the passage a choice leads to shows its outcome
|
|
228
|
+
|
|
229
|
+
THIS STORY MUST BE PURE BRANCHING — NO STATE:
|
|
230
|
+
- Do NOT declare or use variables (no VAR lines, no ~ assignments)
|
|
231
|
+
- Do NOT use conditional text or logic (no { ... } blocks of any kind)
|
|
232
|
+
- A choice's consequence is carried by the knot it diverts to, not by tracked state
|
|
233
|
+
- This keeps the story convertible to tree-based formats (H5P Branching Scenario, LMS embeds)
|
|
234
|
+
|
|
235
|
+
When generating Ink, follow these rules:
|
|
236
|
+
- Always start the file with a -> start divert (no VAR declarations before it)
|
|
237
|
+
- The first knot MUST be named === start ===
|
|
238
|
+
- Use === knot_name === for passages (knots)
|
|
239
|
+
- Use = stitch_name for sub-sections within knots
|
|
240
|
+
- Use * for once-only choices, + for sticky choices
|
|
241
|
+
- Use -> knot_name for diverts
|
|
242
|
+
- Always include a -> END or -> DONE to terminate paths
|
|
243
|
+
- Use # IMAGE: and # TIMER: tags for Playable Lessons custom features`,
|
|
244
|
+
outlineBranching: `Create a detailed story outline for an interactive fiction based on this:
|
|
245
|
+
|
|
246
|
+
Input mode: {{inputMode}}
|
|
247
|
+
Source material: """{{inputText}}"""
|
|
248
|
+
Story length: {{storyLength}} (short=~5 nodes, medium=~15, long=~30+)
|
|
249
|
+
Protagonist type: {{protagonistType}}
|
|
250
|
+
Tone: {{tone}}
|
|
251
|
+
|
|
252
|
+
User's answers to clarification questions:
|
|
253
|
+
{{answers}}
|
|
254
|
+
|
|
255
|
+
Generate a JSON outline with this structure:
|
|
256
|
+
\`\`\`json
|
|
257
|
+
{
|
|
258
|
+
"nodes": [
|
|
259
|
+
{ "id": "start", "title": "Opening Scene", "summary": "Brief description" }
|
|
260
|
+
],
|
|
261
|
+
"edges": [
|
|
262
|
+
{ "from": "start", "to": "next_node", "choiceText": "What the player clicks" }
|
|
263
|
+
],
|
|
264
|
+
"canonPath": ["start", "node2", "node3", "best_ending"]
|
|
265
|
+
}
|
|
266
|
+
\`\`\`
|
|
267
|
+
|
|
268
|
+
Ensure:
|
|
269
|
+
- Every node is reachable from "start"
|
|
270
|
+
- Every path reaches an ending node
|
|
271
|
+
- Do NOT include variables — this story is a pure branching tree, and every
|
|
272
|
+
consequence must be expressed by which node a choice leads to
|
|
273
|
+
- Choices lead to genuinely different passages, not cosmetic variations
|
|
274
|
+
- The story has at least 2 distinct endings
|
|
275
|
+
- The canon path represents the "ideal" learning path`,
|
|
276
|
+
inkGenerationBranching: `Convert this story outline into valid Ink source code.
|
|
277
|
+
|
|
278
|
+
Outline:
|
|
279
|
+
{{outline}}
|
|
280
|
+
|
|
281
|
+
Original source material (for factual accuracy):
|
|
282
|
+
Input mode: {{inputMode}}
|
|
283
|
+
"""{{inputText}}"""
|
|
284
|
+
|
|
285
|
+
Target length: {{storyLength}}
|
|
286
|
+
|
|
287
|
+
Requirements:
|
|
288
|
+
- Begin the file with a -> start divert (do NOT declare any VAR)
|
|
289
|
+
- Each node from the outline becomes an === knot ===
|
|
290
|
+
- Each edge becomes a choice (* [choice text])
|
|
291
|
+
- Do NOT use variables, ~ assignments, or conditional text { ... } anywhere —
|
|
292
|
+
this story must remain a pure branching tree
|
|
293
|
+
- Let each passage's prose reflect the choice that led to it, so consequences
|
|
294
|
+
live in the branches rather than in tracked state
|
|
295
|
+
- Every path must reach -> END
|
|
296
|
+
- Write engaging, educational prose — not just placeholder text
|
|
297
|
+
- Write ALL prose yourself. Do NOT copy any example or placeholder text from the
|
|
298
|
+
syntax reference verbatim (e.g. "[Write the opening scene here]"); those
|
|
299
|
+
bracketed placeholders show structure only — replace them with real content
|
|
300
|
+
- Mark endings with a # ENDING: tag followed by a label (good/neutral/bad)
|
|
301
|
+
|
|
302
|
+
Return ONLY the complete Ink source inside a \`\`\`ink code block.`,
|
|
303
|
+
inkSyntaxRefBranching: `=== INK SYNTAX REFERENCE (PURE BRANCHING — no variables, no conditionals) ===
|
|
304
|
+
|
|
305
|
+
// Start the file with a divert to the first knot (no VAR declarations)
|
|
306
|
+
-> start
|
|
307
|
+
|
|
308
|
+
// First knot MUST be named "start"
|
|
309
|
+
// (bracketed text below = placeholders — replace with your own prose, do not copy)
|
|
310
|
+
=== start ===
|
|
311
|
+
[Write the opening scene here]
|
|
312
|
+
* [First choice] -> next_scene
|
|
313
|
+
|
|
314
|
+
// Other knots
|
|
315
|
+
=== knot_name ===
|
|
316
|
+
[Write the passage prose here]
|
|
317
|
+
* [Choice text visible to player]
|
|
318
|
+
[Result prose shown after choosing]
|
|
319
|
+
-> next_knot
|
|
320
|
+
* [Another choice]
|
|
321
|
+
-> another_knot
|
|
322
|
+
|
|
323
|
+
// Stitches (sub-sections within a knot)
|
|
324
|
+
=== knot ===
|
|
325
|
+
= stitch_one
|
|
326
|
+
[Stitch prose here]
|
|
327
|
+
-> stitch_two
|
|
328
|
+
= stitch_two
|
|
329
|
+
[More stitch prose here]
|
|
330
|
+
|
|
331
|
+
// Sticky choices (can be chosen repeatedly)
|
|
332
|
+
+ [Repeatable choice]
|
|
333
|
+
|
|
334
|
+
// Diverts
|
|
335
|
+
-> knot_name
|
|
336
|
+
-> END
|
|
337
|
+
-> DONE
|
|
338
|
+
|
|
339
|
+
// Tags (for Playable Lessons)
|
|
340
|
+
# IMAGE: assets/images/scene.jpg
|
|
341
|
+
# IMAGE_PROMPT: a short visual description of the scene, used to generate an illustration
|
|
342
|
+
# TIMER: 15
|
|
343
|
+
# ENDING: good
|
|
344
|
+
|
|
345
|
+
// Glue (prevents line break)
|
|
346
|
+
Text<>
|
|
347
|
+
<>more text on same line
|
|
348
|
+
|
|
349
|
+
// FORBIDDEN in this style — do not emit any of these:
|
|
350
|
+
// VAR score = 0
|
|
351
|
+
// ~ score = score + 1
|
|
352
|
+
// { score > 5: ... }
|
|
353
|
+
// You feel {has_key: confident|nervous}.
|
|
354
|
+
|
|
355
|
+
=== END ===`,
|
|
356
|
+
flashcardsSystem: `You are the Playable Lessons assistant, an AI that transforms educational source material into concise, effective study flashcards.
|
|
357
|
+
|
|
358
|
+
Your goals:
|
|
359
|
+
- Surface the most important, learnable facts and concepts in the source material
|
|
360
|
+
- Phrase each front as a clear question, term, or prompt; keep it short and unambiguous
|
|
361
|
+
- Make each back a precise, self-contained answer that is understandable without seeing the source
|
|
362
|
+
- Preserve factual accuracy — never invent facts not supported by the source
|
|
363
|
+
- Avoid trivial cards (common-word definitions) unless they are genuinely the point
|
|
364
|
+
- Use a short tag to group related cards (e.g. "definitions", "process", "people")
|
|
365
|
+
|
|
366
|
+
Respond with ONLY the JSON object requested — no preamble, no commentary.`,
|
|
367
|
+
flashcards: `Create a set of study flashcards from the following source material.
|
|
368
|
+
|
|
369
|
+
Input mode: {{inputMode}}
|
|
370
|
+
Source material:
|
|
371
|
+
"""
|
|
372
|
+
{{inputText}}
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
Target number of cards: {{cardCount}}
|
|
376
|
+
Tone: {{tone}}
|
|
377
|
+
|
|
378
|
+
Return ONLY this JSON shape (wrapped in a \`\`\`json block is fine):
|
|
379
|
+
{
|
|
380
|
+
"deckTitle": "a short title for the deck",
|
|
381
|
+
"cards": [
|
|
382
|
+
{
|
|
383
|
+
"front": "a question, term, or prompt",
|
|
384
|
+
"back": "the precise answer or explanation",
|
|
385
|
+
"hint": "optional nudge; omit the key entirely if not useful",
|
|
386
|
+
"tag": "optional short topic tag"
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
Rules:
|
|
392
|
+
- Produce close to {{cardCount}} cards; every card needs a non-empty front and back
|
|
393
|
+
- "hint" and "tag" are optional — omit the key rather than sending null or an empty string
|
|
394
|
+
- Do not output any text outside the JSON object`,
|
|
395
|
+
quizSystem: `You are the Playable Lessons assistant, an AI that transforms educational source material into fair, well-formed multiple-choice quiz questions.
|
|
396
|
+
|
|
397
|
+
Your goals:
|
|
398
|
+
- Test genuine understanding, not trivia or word-matching
|
|
399
|
+
- Write a clear, specific stem (the question) that is unambiguous
|
|
400
|
+
- Provide plausible distractors that are clearly wrong but not absurd
|
|
401
|
+
- Make exactly one option unambiguously correct
|
|
402
|
+
- Vary which position (A/B/C/D) holds the correct answer across questions
|
|
403
|
+
- Preserve factual accuracy — never invent facts not supported by the source
|
|
404
|
+
- Add a short explanation for each answer justifying why the correct option is right
|
|
405
|
+
|
|
406
|
+
Respond with ONLY the JSON object requested — no preamble, no commentary.`,
|
|
407
|
+
quiz: `Create a set of multiple-choice quiz questions from the following source material.
|
|
408
|
+
|
|
409
|
+
Input mode: {{inputMode}}
|
|
410
|
+
Source material:
|
|
411
|
+
"""
|
|
412
|
+
{{inputText}}
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
Target number of questions: {{questionCount}}
|
|
416
|
+
Tone: {{tone}}
|
|
417
|
+
|
|
418
|
+
Return ONLY this JSON shape (wrapped in a \`\`\`json block is fine):
|
|
419
|
+
{
|
|
420
|
+
"quizTitle": "a short title for the quiz",
|
|
421
|
+
"questions": [
|
|
422
|
+
{
|
|
423
|
+
"stem": "the question text",
|
|
424
|
+
"options": ["choice A", "choice B", "choice C", "choice D"],
|
|
425
|
+
"correctIndex": 0,
|
|
426
|
+
"explanation": "why the correct option is right (optional but encouraged)"
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
Rules:
|
|
432
|
+
- Produce close to {{questionCount}} questions
|
|
433
|
+
- Each question needs 2-6 options; exactly one must be correct
|
|
434
|
+
- "correctIndex" is the 0-based index of the correct option within "options"
|
|
435
|
+
- Vary the correct option's position across questions
|
|
436
|
+
- "explanation" is optional — omit the key rather than sending null or an empty string
|
|
437
|
+
- Do not output any text outside the JSON object`,
|
|
438
|
+
summarySystem: `You are the Playable Lessons assistant, an AI that distills educational source material into a clear, accurate study summary.
|
|
439
|
+
|
|
440
|
+
Your goals:
|
|
441
|
+
- Capture the essence: the main subject and why it matters
|
|
442
|
+
- Pull out the most important, learnable key points (not trivia)
|
|
443
|
+
- Define the essential terms a learner must know
|
|
444
|
+
- Preserve factual accuracy — never invent facts not supported by the source
|
|
445
|
+
- Be concise and concrete; write prose a learner can actually use
|
|
446
|
+
|
|
447
|
+
Respond with ONLY the JSON object requested — no preamble, no commentary.`,
|
|
448
|
+
summary: `Create a study summary from the following source material.
|
|
449
|
+
|
|
450
|
+
Input mode: {{inputMode}}
|
|
451
|
+
Source material:
|
|
452
|
+
"""
|
|
453
|
+
{{inputText}}
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
Target number of key points: {{keyPointCount}}
|
|
457
|
+
Tone: {{tone}}
|
|
458
|
+
|
|
459
|
+
Return ONLY this JSON shape (wrapped in a \`\`\`json block is fine):
|
|
460
|
+
{
|
|
461
|
+
"title": "a short title for the summary",
|
|
462
|
+
"overview": "1-3 sentences capturing the essence of the material",
|
|
463
|
+
"keyPoints": ["a concise, self-contained point", "..."],
|
|
464
|
+
"glossary": [{ "term": "a key term", "definition": "a short definition" }]
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
Rules:
|
|
468
|
+
- Produce close to {{keyPointCount}} key points; each must be non-empty and self-contained
|
|
469
|
+
- Include only terms central to understanding in "glossary" (5-12 entries is typical)
|
|
470
|
+
- Omit a key entirely rather than sending null or an empty string
|
|
471
|
+
- Do not output any text outside the JSON object`,
|
|
472
|
+
aiTaskSystem: `You are the Playable Lessons assistant. You re-engineer educational source material (a worksheet, lesson, or rough idea) into AI-collaboration tasks — tasks a learner completes by working iteratively with an LLM chatbot.
|
|
473
|
+
|
|
474
|
+
THE DEFINING REQUIREMENT: every task must be one where critically engaging the chatbot — probing, challenging, revising its answers — is what produces a good result. A task where the AI's first answer is already complete, or where a learner could do well by pasting that answer, is a FAILURE no matter how polished it looks.
|
|
475
|
+
|
|
476
|
+
A task qualifies ONLY if it is:
|
|
477
|
+
- Tool-mediated: the learner uses the chatbot as a working tool; the task is not about AI itself.
|
|
478
|
+
- Iterative: one prompt and one answer cannot do it well.
|
|
479
|
+
- First-answer-defeatable: the brief contains specifics the AI cannot see that make a generic answer wrong or incomplete.
|
|
480
|
+
- Artifact-producing: it ends in a deliverable judged on the learner's reasoning.
|
|
481
|
+
- Engagement-visible: a reader can tell an interrogating learner from a delegating one.
|
|
482
|
+
|
|
483
|
+
NEVER propose: quizzes, flashcards, fact-lookup, "use AI to learn/summarise topic X", anything solvable in one prompt, or anything about AI as a subject. (Those other outputs exist as separate targets in this tool; THIS output is exclusively AI-collaboration tasks.)
|
|
484
|
+
|
|
485
|
+
THE MECHANISM THAT MAKES INTERROGATION PAY OFF: load-bearing specifics. Build 2-4 concrete details into each scenario — a constraint, a conflicting stakeholder, an awkward number, a clause — that a generic answer gets wrong or ignores. The learner sees the scenario; the chatbot does not, until the learner surfaces those specifics through interrogation.
|
|
486
|
+
|
|
487
|
+
Respond with ONLY the JSON object requested — no preamble, no commentary.`,
|
|
488
|
+
aiTask: `Re-engineer the following source material into {{taskCount}} AI-collaboration tasks.
|
|
489
|
+
|
|
490
|
+
Input mode: {{inputMode}}
|
|
491
|
+
Source material:
|
|
492
|
+
"""
|
|
493
|
+
{{inputText}}
|
|
494
|
+
"""
|
|
495
|
+
Tone: {{tone}}
|
|
496
|
+
|
|
497
|
+
Return ONLY this JSON shape (wrapped in a \`\`\`json block is fine):
|
|
498
|
+
{
|
|
499
|
+
"title": "a short title for this set of tasks",
|
|
500
|
+
"tasks": [
|
|
501
|
+
{
|
|
502
|
+
"scenario": "a concrete situation that embeds 2-4 load-bearing specifics",
|
|
503
|
+
"brief": "the student-facing instruction, readable with zero chatbot experience",
|
|
504
|
+
"deliverable": "the human-authored output the learner must produce",
|
|
505
|
+
"loadBearingSpecifics": [
|
|
506
|
+
{ "detail": "a concrete specific built into the scenario", "whyGenericAnswersMissIt": "why a generic AI answer gets this wrong or ignores it" }
|
|
507
|
+
],
|
|
508
|
+
"rubric": [
|
|
509
|
+
{ "criterion": "a criterion anchored on engagement with the specifics", "description": "what strong engagement looks like" }
|
|
510
|
+
],
|
|
511
|
+
"whyItWorks": "why a delegating learner loses and an interrogating one wins"
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
Rules:
|
|
517
|
+
- Produce close to {{taskCount}} tasks; every task must satisfy ALL five qualifying criteria
|
|
518
|
+
- Each task needs 2-4 loadBearingSpecifics and 2-4 rubric criteria
|
|
519
|
+
- Rubric criteria must be anchored on engagement with the specifics, NOT on the correctness of the final answer
|
|
520
|
+
- The scenario carries the load-bearing specifics (learner-visible); a chatbot that has not been told them must get the task wrong
|
|
521
|
+
- Omit an array element's key rather than sending null or an empty string
|
|
522
|
+
- Do not output any text outside the JSON object`,
|
|
523
|
+
caseStudySystem: `You are the Playable Lessons assistant. You turn educational source material (a topic, lesson, methodology, or rough idea) into a teaching case study — a realistic situation a learner analyzes to surface and apply the key concepts.
|
|
524
|
+
|
|
525
|
+
A good teaching case study:
|
|
526
|
+
- Is a concrete, specific situation (real or plausible), not a summary of theory
|
|
527
|
+
- Has a protagonist facing a real decision or dilemma
|
|
528
|
+
- Embeds the key concepts as details the learner must notice and reason about
|
|
529
|
+
- Carries just enough data / numbers / context to force analysis, not recitation
|
|
530
|
+
- Ends open-ended (a decision to weigh), not with "the answer"
|
|
531
|
+
- Stays factually grounded — never invent specifics presented as real events, and never fabricate named real people or institutions
|
|
532
|
+
|
|
533
|
+
Respond with ONLY the JSON object requested — no preamble, no commentary.`,
|
|
534
|
+
caseStudy: `Create a teaching case study from the following source material.
|
|
535
|
+
|
|
536
|
+
Input mode: {{inputMode}}
|
|
537
|
+
Source material:
|
|
538
|
+
"""
|
|
539
|
+
{{inputText}}
|
|
540
|
+
"""
|
|
541
|
+
Depth: {{depth}}
|
|
542
|
+
Tone: {{tone}}
|
|
543
|
+
|
|
544
|
+
Return ONLY this JSON shape (wrapped in a \`\`\`json block is fine):
|
|
545
|
+
{
|
|
546
|
+
"title": "a short, specific title for the case",
|
|
547
|
+
"protagonist": "who/what the case centers on (a role, team, or organization — fictional unless the source names one)",
|
|
548
|
+
"situation": "the concrete setup and context",
|
|
549
|
+
"keyFacts": ["a specific data point, number, constraint, or exhibit the learner must weigh", "..."],
|
|
550
|
+
"conflict": "the central tension or dilemma",
|
|
551
|
+
"decisionPoints": ["a juncture or choice the protagonist faces", "..."],
|
|
552
|
+
"discussionQuestions": ["an open question that forces analysis (not recall)", "..."],
|
|
553
|
+
"narrative": "the full case written as connected prose — OMIT this key entirely unless depth is 'complete'"
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
Depth rules:
|
|
557
|
+
- idea: keep situation, conflict, and decisionPoints brief (a premise); 1-2 keyFacts; omit narrative
|
|
558
|
+
- outline: fill every structural field concretely, but omit narrative (this is a skeleton)
|
|
559
|
+
- complete: fill every structural field AND write a full "narrative" (several paragraphs) weaving them together
|
|
560
|
+
- discussionQuestions: 2-4, always open-ended
|
|
561
|
+
- Omit the "narrative" key entirely unless depth is "complete"; never send null or empty strings
|
|
562
|
+
- Do not output any text outside the JSON object`,
|
|
563
|
+
planSystem: `You are the Playable Lessons planning assistant. You analyze educational source material and recommend a complementary SET of outputs an educator could generate from it.
|
|
564
|
+
|
|
565
|
+
Available output targets and what each is good for:
|
|
566
|
+
- story: a branching, playable interactive-fiction narrative (immersive practice; best for decision-making, ethics, scenarios)
|
|
567
|
+
- flashcards: spaced-recall study cards (best for terminology, definitions, recall)
|
|
568
|
+
- quiz: self-marking multiple-choice questions (best for checking comprehension)
|
|
569
|
+
- summary: a concise study summary with key points + glossary (best for overview / revision)
|
|
570
|
+
- ai-task: AI-collaboration tasks where the learner interrogates a chatbot (best for critical thinking, reasoning)
|
|
571
|
+
- case-study: a teaching case study with a dilemma + discussion questions (best for applied analysis)
|
|
572
|
+
|
|
573
|
+
Recommend 2-4 complementary targets that together serve the material — not all six, and not redundant ones. Pick the combination that fits the content and a likely learning goal.
|
|
574
|
+
|
|
575
|
+
Respond with ONLY the JSON object requested — no preamble, no commentary.`,
|
|
576
|
+
plan: `Analyze the following source material and recommend a set of outputs to generate from it.
|
|
577
|
+
|
|
578
|
+
Input mode: {{inputMode}}
|
|
579
|
+
Source material:
|
|
580
|
+
"""
|
|
581
|
+
{{inputText}}
|
|
582
|
+
"""
|
|
583
|
+
Tone: {{tone}}
|
|
584
|
+
|
|
585
|
+
Return ONLY this JSON shape (wrapped in a \`\`\`json block is fine):
|
|
586
|
+
{
|
|
587
|
+
"title": "a short title for the material",
|
|
588
|
+
"summary": "1-2 sentences on what the material is and the likely learning goal",
|
|
589
|
+
"recommendations": [
|
|
590
|
+
{
|
|
591
|
+
"target": "one of: story | flashcards | quiz | summary | ai-task | case-study",
|
|
592
|
+
"rationale": "why this target suits this material",
|
|
593
|
+
"depth": "for case-study only: idea | outline | complete",
|
|
594
|
+
"count": "optional number of items (cards / questions / key points / tasks)"
|
|
595
|
+
}
|
|
596
|
+
]
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
Rules:
|
|
600
|
+
- Recommend 2-4 targets; each target must be one of the six listed
|
|
601
|
+
- Order them by usefulness for this material
|
|
602
|
+
- Include "depth" only when target is "case-study"; include "count" only when relevant — omit them otherwise
|
|
603
|
+
- Do not output any text outside the JSON object`
|
|
194
604
|
};
|
|
605
|
+
/**
|
|
606
|
+
* The style-dependent story prompts. Both the interactive GUI flow and the
|
|
607
|
+
* headless pipeline (CLI, web server) must select prompts through this so a
|
|
608
|
+
* given style generates identically everywhere. Analysis/clarification/review
|
|
609
|
+
* prompts are shared across styles.
|
|
610
|
+
*/
|
|
611
|
+
function storyPrompts(style = 'stateful') {
|
|
612
|
+
if (style === 'branching') {
|
|
613
|
+
return {
|
|
614
|
+
system: exports.PROMPTS.systemBranching,
|
|
615
|
+
outline: exports.PROMPTS.outlineBranching,
|
|
616
|
+
inkGeneration: exports.PROMPTS.inkGenerationBranching,
|
|
617
|
+
inkSyntaxRef: exports.PROMPTS.inkSyntaxRefBranching
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
system: exports.PROMPTS.system,
|
|
622
|
+
outline: exports.PROMPTS.outline,
|
|
623
|
+
inkGeneration: exports.PROMPTS.inkGeneration,
|
|
624
|
+
inkSyntaxRef: exports.PROMPTS.inkSyntaxRef
|
|
625
|
+
};
|
|
626
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Quiz export formats, shared by the renderer and the CLI.
|
|
4
|
+
*
|
|
5
|
+
* Standalone (no runtime imports from sibling shared modules) — only the
|
|
6
|
+
* QuizResult *type* is imported, which is erased at compile time, so it resolves
|
|
7
|
+
* under both bundler (renderer) and NodeNext (CLI) module resolution.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.toPlainText = toPlainText;
|
|
11
|
+
exports.toStandaloneHTML = toStandaloneHTML;
|
|
12
|
+
const generate_js_1 = require("./generate.js");
|
|
13
|
+
/**
|
|
14
|
+
* A printable quiz with a separate answer key — for handouts or offline review.
|
|
15
|
+
* Questions are numbered; options are lettered A, B, C, …
|
|
16
|
+
*/
|
|
17
|
+
function toPlainText(result, title) {
|
|
18
|
+
const heading = result.quizTitle || title || 'Quiz';
|
|
19
|
+
const lines = [heading, ''];
|
|
20
|
+
result.questions.forEach((q, i) => {
|
|
21
|
+
lines.push(`${i + 1}. ${q.stem}`);
|
|
22
|
+
q.options.forEach((opt, j) => {
|
|
23
|
+
lines.push(` ${(0, generate_js_1.quizLetter)(j)}) ${opt}`);
|
|
24
|
+
});
|
|
25
|
+
lines.push('');
|
|
26
|
+
});
|
|
27
|
+
lines.push('--- Answer Key ---');
|
|
28
|
+
result.questions.forEach((q, i) => {
|
|
29
|
+
const entry = `${i + 1}. ${(0, generate_js_1.quizLetter)(q.correctIndex)}${q.explanation ? ` — ${q.explanation}` : ''}`;
|
|
30
|
+
lines.push(entry);
|
|
31
|
+
});
|
|
32
|
+
return lines.join('\n') + '\n';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A self-contained, offline HTML quiz. Each question is multiple-choice; on
|
|
36
|
+
* submit it grades the answers, shows a score, marks each option correct/wrong,
|
|
37
|
+
* and reveals explanations. Option/question text is rendered via textContent
|
|
38
|
+
* (no innerHTML), and the embedded question JSON is guarded against breaking out
|
|
39
|
+
* of its <script> by escaping '<' to '\u003c'.
|
|
40
|
+
*/
|
|
41
|
+
function toStandaloneHTML(result, title) {
|
|
42
|
+
const quizTitle = result.quizTitle || title;
|
|
43
|
+
const dataJson = JSON.stringify(result.questions).replace(/</g, '\\u003c');
|
|
44
|
+
const safeTitle = String(quizTitle)
|
|
45
|
+
.replace(/&/g, '&')
|
|
46
|
+
.replace(/</g, '<')
|
|
47
|
+
.replace(/>/g, '>')
|
|
48
|
+
.replace(/"/g, '"')
|
|
49
|
+
.replace(/'/g, ''');
|
|
50
|
+
return `<!DOCTYPE html>
|
|
51
|
+
<html lang="en">
|
|
52
|
+
<head>
|
|
53
|
+
<meta charset="UTF-8">
|
|
54
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
55
|
+
<title>${safeTitle} — Quiz</title>
|
|
56
|
+
<style>
|
|
57
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
58
|
+
body { font-family: Georgia, 'Times New Roman', serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 40px 20px; }
|
|
59
|
+
#wrap { max-width: 640px; width: 100%; }
|
|
60
|
+
h1 { font-size: 26px; margin-bottom: 4px; color: #a78bfa; text-align: center; }
|
|
61
|
+
.progress-bar { height: 4px; background: #252547; border-radius: 2px; margin: 16px 0 24px; overflow: hidden; }
|
|
62
|
+
.progress-fill { height: 100%; background: #a78bfa; border-radius: 2px; transition: width 0.3s ease; }
|
|
63
|
+
.progress-text { text-align: center; font-size: 14px; color: #888; margin-bottom: 20px; }
|
|
64
|
+
.question-card { background: #252547; border: 1px solid #3a3a5c; border-radius: 14px; padding: 28px 26px; margin-bottom: 20px; }
|
|
65
|
+
.question-card .stem { font-size: 19px; line-height: 1.5; margin-bottom: 18px; }
|
|
66
|
+
.option { display: flex; gap: 12px; align-items: flex-start; padding: 12px 14px; margin-top: 8px; border: 1px solid transparent; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
|
|
67
|
+
.option:hover { border-color: #3a3a5c; background: rgba(167,139,250,0.05); }
|
|
68
|
+
.option input { margin-top: 3px; accent-color: #a78bfa; cursor: pointer; }
|
|
69
|
+
.option span { line-height: 1.5; font-size: 16px; }
|
|
70
|
+
.option.correct { border-color: #4ade80; background: rgba(74,222,128,0.12); }
|
|
71
|
+
.option.wrong { border-color: #f87171; background: rgba(248,113,113,0.12); }
|
|
72
|
+
.option.selected { border-color: #a78bfa; background: rgba(167,139,250,0.1); }
|
|
73
|
+
.explanation { display: none; margin-top: 12px; padding: 12px 14px; background: rgba(167,139,250,0.1); border-left: 3px solid #a78bfa; color: #c4b5fd; font-size: 15px; border-radius: 0 6px 6px 0; font-style: italic; }
|
|
74
|
+
.score-display { text-align: center; padding: 20px; }
|
|
75
|
+
.score-display .score-num { font-size: 36px; font-weight: 700; color: #a78bfa; }
|
|
76
|
+
.score-display .score-label { font-size: 16px; color: #888; margin-bottom: 20px; }
|
|
77
|
+
.controls { display: flex; gap: 10px; justify-content: center; margin-top: 20px; }
|
|
78
|
+
button { padding: 14px 32px; background: #a78bfa; color: #1a1a2e; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; font-family: inherit; transition: background 0.15s; }
|
|
79
|
+
button:hover:not(:disabled) { background: #8b6fdf; }
|
|
80
|
+
button:disabled { opacity: 0.4; cursor: default; }
|
|
81
|
+
button.secondary { background: #252547; color: #e0e0e0; border: 1px solid #3a3a5c; font-weight: 400; }
|
|
82
|
+
button.secondary:hover:not(:disabled) { border-color: #a78bfa; }
|
|
83
|
+
.meta { text-align: center; color: #555; font-size: 12px; margin-top: 32px; }
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<div id="wrap">
|
|
88
|
+
<h1>${safeTitle}</h1>
|
|
89
|
+
<div id="content"></div>
|
|
90
|
+
</div>
|
|
91
|
+
<script>
|
|
92
|
+
(function () {
|
|
93
|
+
var ALL = ${dataJson};
|
|
94
|
+
var QUIZ_SIZE = 5;
|
|
95
|
+
var deck = [], pos = 0, answers = {}, submitted = false;
|
|
96
|
+
var content = document.getElementById('content');
|
|
97
|
+
|
|
98
|
+
function shuffle(arr) {
|
|
99
|
+
var a = arr.slice();
|
|
100
|
+
for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random()*(i+1)); var t = a[i]; a[i]=a[j]; a[j]=t; }
|
|
101
|
+
return a;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function newDeck() {
|
|
105
|
+
deck = ALL.length <= QUIZ_SIZE ? shuffle(ALL) : shuffle(ALL).slice(0, QUIZ_SIZE);
|
|
106
|
+
pos = 0; answers = {}; submitted = false;
|
|
107
|
+
renderQuestion();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderQuestion() {
|
|
111
|
+
var q = deck[pos];
|
|
112
|
+
var html = '<div class="progress-bar"><div class="progress-fill" style="width:' + (pos / deck.length * 100) + '%"></div></div>';
|
|
113
|
+
html += '<div class="progress-text">Question ' + (pos + 1) + ' of ' + deck.length + '</div>';
|
|
114
|
+
html += '<div class="question-card"><div class="stem">' + esc(q.stem) + '</div>';
|
|
115
|
+
html += '<div id="options">';
|
|
116
|
+
q.options.forEach(function (opt, oi) {
|
|
117
|
+
var sel = answers[pos] === oi ? ' selected' : '';
|
|
118
|
+
html += '<div class="option' + sel + '" data-oi="' + oi + '"><input type="radio" name="q" id="o' + oi + '"' + (answers[pos] === oi ? ' checked' : '') + '><span>' + letters(oi) + ') ' + esc(opt) + '</span></div>';
|
|
119
|
+
});
|
|
120
|
+
html += '</div>';
|
|
121
|
+
if (q.explanation) html += '<div class="explanation" id="exp">' + esc(q.explanation) + '</div>';
|
|
122
|
+
html += '</div>';
|
|
123
|
+
html += '<div class="controls">';
|
|
124
|
+
if (pos > 0) html += '<button class="secondary" id="prev">‹ Prev</button>';
|
|
125
|
+
if (pos < deck.length - 1) html += '<button id="next">Next ›</button>';
|
|
126
|
+
else html += '<button id="submit">Submit</button>';
|
|
127
|
+
html += '</div>';
|
|
128
|
+
content.innerHTML = html;
|
|
129
|
+
|
|
130
|
+
content.querySelectorAll('.option').forEach(function (el) {
|
|
131
|
+
el.addEventListener('click', function () {
|
|
132
|
+
if (submitted) return;
|
|
133
|
+
answers[pos] = parseInt(el.dataset.oi);
|
|
134
|
+
content.querySelectorAll('.option').forEach(function (e) { e.classList.remove('selected'); });
|
|
135
|
+
el.classList.add('selected');
|
|
136
|
+
el.querySelector('input').checked = true;
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
var prev = document.getElementById('prev'); if (prev) prev.addEventListener('click', function () { pos--; renderQuestion(); });
|
|
140
|
+
var next = document.getElementById('next'); if (next) next.addEventListener('click', function () { pos++; renderQuestion(); });
|
|
141
|
+
var sub = document.getElementById('submit'); if (sub) sub.addEventListener('click', grade);
|
|
142
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function grade() {
|
|
146
|
+
submitted = true;
|
|
147
|
+
var score = 0;
|
|
148
|
+
deck.forEach(function (q, qi) { if (answers[qi] === q.correctIndex) score++; });
|
|
149
|
+
|
|
150
|
+
var html = '<div class="score-display"><div class="score-num">' + score + ' / ' + deck.length + '</div><div class="score-label">Score</div></div>';
|
|
151
|
+
deck.forEach(function (q, qi) {
|
|
152
|
+
var chosen = answers[qi];
|
|
153
|
+
var correct = chosen === q.correctIndex;
|
|
154
|
+
html += '<div class="question-card"><div class="stem">' + (qi+1) + '. ' + esc(q.stem) + '</div>';
|
|
155
|
+
q.options.forEach(function (opt, oi) {
|
|
156
|
+
var cls = 'option';
|
|
157
|
+
if (oi === q.correctIndex) cls += ' correct';
|
|
158
|
+
else if (oi === chosen) cls += ' wrong';
|
|
159
|
+
html += '<div class="' + cls + '"><span>' + letters(oi) + ') ' + esc(opt);
|
|
160
|
+
if (oi === q.correctIndex) html += ' \\u2705';
|
|
161
|
+
html += '</span></div>';
|
|
162
|
+
});
|
|
163
|
+
if (q.explanation) html += '<div class="explanation" style="display:block">' + esc(q.explanation) + '</div>';
|
|
164
|
+
html += '</div>';
|
|
165
|
+
});
|
|
166
|
+
html += '<div class="controls"><button id="again">Try again (new questions)</button></div>';
|
|
167
|
+
content.innerHTML = html;
|
|
168
|
+
document.getElementById('again').addEventListener('click', newDeck);
|
|
169
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function letters(i) { return 'ABCDEFGHIJ'[i] || (i+1); }
|
|
173
|
+
function esc(t) { var d = document.createElement('div'); d.textContent = String(t||''); return d.innerHTML; }
|
|
174
|
+
|
|
175
|
+
newDeck();
|
|
176
|
+
var meta = document.createElement('div'); meta.className = 'meta'; meta.textContent = 'Created with Playable Lessons';
|
|
177
|
+
document.getElementById('wrap').appendChild(meta);
|
|
178
|
+
})();
|
|
179
|
+
</script>
|
|
180
|
+
</body>
|
|
181
|
+
</html>`;
|
|
182
|
+
}
|