playable-lessons 0.2.1 → 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/inkRuntime.generated.js +9 -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 +72 -20
- package/out/shared/summaryExport.js +144 -0
- package/out/shared/version.generated.js +7 -0
- package/package.json +9 -5
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Shared
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* NodeNext module resolution.
|
|
3
|
+
* Shared story logic used by BOTH the renderer (via Vite) and the CLI (via
|
|
4
|
+
* tsc/NodeNext). Intra-shared imports use explicit `.js` extensions so they
|
|
5
|
+
* resolve under both bundler and NodeNext module resolution.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Side dependencies: a dynamic import of inkjs's Compiler, and the embedded
|
|
8
|
+
* inkjs runtime (so HTML exports are fully offline).
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.escapeHtml = escapeHtml;
|
|
12
12
|
exports.ensureStartDivert = ensureStartDivert;
|
|
13
13
|
exports.compileInk = compileInk;
|
|
14
14
|
exports.exportStandaloneHTML = exportStandaloneHTML;
|
|
15
|
-
|
|
16
|
-
// compiles with (see package.json `inkjs`) so the compiled JSON format matches
|
|
17
|
-
// the runtime that plays it, and lock it down with a Subresource Integrity hash.
|
|
18
|
-
const INKJS_RUNTIME_URL = 'https://cdn.jsdelivr.net/npm/inkjs@2.4.0/dist/ink.js';
|
|
19
|
-
const INKJS_RUNTIME_SRI = 'sha384-wR7zU2DW6Ji/8kGVhTaLk0TSOucJhD4Zhv7lR5G1k/HoXtcWhEw5nIlcyE/+GtMb';
|
|
15
|
+
const inkRuntime_generated_js_1 = require("./inkRuntime.generated.js");
|
|
20
16
|
/**
|
|
21
17
|
* Escape a string for safe interpolation into HTML text or double-quoted
|
|
22
18
|
* attribute values. Escapes &, <, >, ", and ' (the single quote matters for
|
|
@@ -30,6 +26,13 @@ function escapeHtml(text) {
|
|
|
30
26
|
.replace(/"/g, '"')
|
|
31
27
|
.replace(/'/g, ''');
|
|
32
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Neutralize any `</script>` sequence so embedded JS/JSON can't break out of
|
|
31
|
+
* its <script> element. Safe to apply to runtime code and JSON alike.
|
|
32
|
+
*/
|
|
33
|
+
function escapeScriptClose(text) {
|
|
34
|
+
return text.replace(/<\/(script)/gi, '<\\/$1');
|
|
35
|
+
}
|
|
33
36
|
/**
|
|
34
37
|
* Ensures the Ink source has a top-level divert to the first knot.
|
|
35
38
|
* Without this, the story starts at the root and immediately ends.
|
|
@@ -54,12 +57,29 @@ function ensureStartDivert(source) {
|
|
|
54
57
|
/**
|
|
55
58
|
* Compiles Ink source to JSON using inkjs's Compiler.
|
|
56
59
|
* Returns the compiled JSON string.
|
|
60
|
+
*
|
|
61
|
+
* inkjs's Compile() throws a bare "Compilation failed." on errors — useless
|
|
62
|
+
* for users and for the AI fix-retry loop (it gets told THAT it failed, never
|
|
63
|
+
* WHAT failed). We collect the compiler's real error messages via an
|
|
64
|
+
* errorHandler and rethrow with them included.
|
|
57
65
|
*/
|
|
58
66
|
async function compileInk(inkSource) {
|
|
59
|
-
const { Compiler } = await import('inkjs/compiler/Compiler');
|
|
67
|
+
const { Compiler, CompilerOptions } = await import('inkjs/compiler/Compiler');
|
|
60
68
|
const source = ensureStartDivert(inkSource);
|
|
61
|
-
const
|
|
62
|
-
const
|
|
69
|
+
const errors = [];
|
|
70
|
+
const compiler = new Compiler(source, new CompilerOptions(null, [], false, (message) => {
|
|
71
|
+
errors.push(message);
|
|
72
|
+
}));
|
|
73
|
+
let story;
|
|
74
|
+
try {
|
|
75
|
+
story = compiler.Compile();
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (errors.length > 0) {
|
|
79
|
+
throw new Error(`Compilation failed: ${errors.slice(0, 5).join(' | ')}`);
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
63
83
|
if (!story) {
|
|
64
84
|
throw new Error('Ink compilation failed: no story produced');
|
|
65
85
|
}
|
|
@@ -73,14 +93,18 @@ async function compileInk(inkSource) {
|
|
|
73
93
|
* Exports a standalone HTML file that plays the story.
|
|
74
94
|
* The inkjs runtime is loaded from a pinned CDN URL (with SRI) in the export.
|
|
75
95
|
*
|
|
76
|
-
*
|
|
77
|
-
* the compiled JSON
|
|
78
|
-
*
|
|
79
|
-
*
|
|
96
|
+
* The inkjs runtime is embedded inline (no CDN), so the file is fully offline.
|
|
97
|
+
* `title` is escaped; the compiled JSON and the runtime are guarded against
|
|
98
|
+
* breaking out of their <script> elements.
|
|
99
|
+
*
|
|
100
|
+
* `sceneImages` maps `# IMAGE_PROMPT:` tag text → data URL (see
|
|
101
|
+
* generateSceneImages); matched scenes render their illustration above the
|
|
102
|
+
* text. `# IMAGE:` tags render only when they carry a data: or http(s) URL.
|
|
80
103
|
*/
|
|
81
|
-
async function exportStandaloneHTML(inkSource, title) {
|
|
104
|
+
async function exportStandaloneHTML(inkSource, title, sceneImages) {
|
|
82
105
|
const compiledJson = await compileInk(inkSource);
|
|
83
|
-
const escapedJson = JSON.stringify(compiledJson);
|
|
106
|
+
const escapedJson = escapeScriptClose(JSON.stringify(compiledJson));
|
|
107
|
+
const escapedImages = escapeScriptClose(JSON.stringify(sceneImages ?? {}));
|
|
84
108
|
const safeTitle = escapeHtml(title);
|
|
85
109
|
return `<!DOCTYPE html>
|
|
86
110
|
<html lang="en">
|
|
@@ -101,6 +125,7 @@ async function exportStandaloneHTML(inkSource, title) {
|
|
|
101
125
|
}
|
|
102
126
|
#story { max-width: 650px; width: 100%; }
|
|
103
127
|
h1 { font-size: 28px; margin-bottom: 32px; color: #a78bfa; text-align: center; }
|
|
128
|
+
.scene-img { max-width: 100%; border-radius: 8px; margin-bottom: 16px; display: block; }
|
|
104
129
|
.text p { font-size: 18px; line-height: 1.8; margin-bottom: 16px; }
|
|
105
130
|
.choices { display: flex; flex-direction: column; gap: 10px; margin-top: 24px; }
|
|
106
131
|
.choice {
|
|
@@ -139,24 +164,51 @@ async function exportStandaloneHTML(inkSource, title) {
|
|
|
139
164
|
<div id="content"></div>
|
|
140
165
|
</div>
|
|
141
166
|
|
|
142
|
-
|
|
167
|
+
<!-- inkjs ${inkRuntime_generated_js_1.INKJS_VERSION} runtime embedded inline so this file plays fully offline -->
|
|
168
|
+
<script>${escapeScriptClose(inkRuntime_generated_js_1.INKJS_RUNTIME)}</script>
|
|
143
169
|
<script>
|
|
144
170
|
(function() {
|
|
145
171
|
var storyJson = ${escapedJson};
|
|
172
|
+
var sceneImages = ${escapedImages};
|
|
146
173
|
var story = new inkjs.Story(storyJson);
|
|
147
174
|
var content = document.getElementById('content');
|
|
148
175
|
|
|
149
176
|
function continueStory() {
|
|
150
177
|
var textDiv = document.createElement('div');
|
|
151
178
|
textDiv.className = 'text';
|
|
179
|
+
var tags = [];
|
|
152
180
|
while (story.canContinue) {
|
|
153
181
|
var line = story.Continue().trim();
|
|
182
|
+
if (story.currentTags) tags = tags.concat(story.currentTags);
|
|
154
183
|
if (line) {
|
|
155
184
|
var p = document.createElement('p');
|
|
156
185
|
p.textContent = line;
|
|
157
186
|
textDiv.appendChild(p);
|
|
158
187
|
}
|
|
159
188
|
}
|
|
189
|
+
|
|
190
|
+
// Scene illustrations: resolve IMAGE_PROMPT tags via the embedded map;
|
|
191
|
+
// IMAGE tags render only when they carry a usable URL/data URL.
|
|
192
|
+
var imgSrcs = [];
|
|
193
|
+
tags.forEach(function(tag) {
|
|
194
|
+
if (tag.indexOf('IMAGE_PROMPT:') === 0) {
|
|
195
|
+
var src = sceneImages[tag.substring('IMAGE_PROMPT:'.length).trim()];
|
|
196
|
+
if (src) imgSrcs.push(src);
|
|
197
|
+
} else if (tag.indexOf('IMAGE:') === 0) {
|
|
198
|
+
var val = tag.substring('IMAGE:'.length).trim();
|
|
199
|
+
// NB: no '//' inside this regex — in this template literal \/ collapses
|
|
200
|
+
// to / and would turn into a line comment in the generated page.
|
|
201
|
+
if (/^(data:|https?:)/.test(val)) imgSrcs.push(val);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
imgSrcs.forEach(function(src) {
|
|
205
|
+
var img = document.createElement('img');
|
|
206
|
+
img.className = 'scene-img';
|
|
207
|
+
img.src = src;
|
|
208
|
+
img.alt = '';
|
|
209
|
+
textDiv.insertBefore(img, textDiv.firstChild);
|
|
210
|
+
});
|
|
211
|
+
|
|
160
212
|
content.appendChild(textDiv);
|
|
161
213
|
|
|
162
214
|
if (story.currentChoices.length > 0) {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Study-summary export formats, shared by the renderer and the CLI.
|
|
4
|
+
*
|
|
5
|
+
* Standalone (no runtime imports from sibling shared modules) — only the
|
|
6
|
+
* SummaryResult *type* is imported, which is erased at compile time, so it
|
|
7
|
+
* resolves 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
|
+
/** A printable study summary: overview, numbered key points, glossary. */
|
|
13
|
+
function toPlainText(result, title) {
|
|
14
|
+
const heading = result.title || title || 'Summary';
|
|
15
|
+
const lines = [heading, ''];
|
|
16
|
+
if (result.overview) {
|
|
17
|
+
lines.push(result.overview, '');
|
|
18
|
+
}
|
|
19
|
+
if (result.keyPoints.length) {
|
|
20
|
+
lines.push('Key Points:');
|
|
21
|
+
result.keyPoints.forEach((p, i) => lines.push(`${i + 1}. ${p}`));
|
|
22
|
+
lines.push('');
|
|
23
|
+
}
|
|
24
|
+
if (result.glossary.length) {
|
|
25
|
+
lines.push('Glossary:');
|
|
26
|
+
result.glossary.forEach((g) => lines.push(`${g.term}: ${g.definition}`));
|
|
27
|
+
lines.push('');
|
|
28
|
+
}
|
|
29
|
+
return lines.join('\n') + '\n';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A self-contained, offline HTML study sheet: title, overview, key points, and
|
|
33
|
+
* a glossary. All text is rendered via textContent (no innerHTML), and the
|
|
34
|
+
* embedded JSON is guarded against breaking out of its <script> by escaping
|
|
35
|
+
* '<' to '\u003c'.
|
|
36
|
+
*/
|
|
37
|
+
function toStandaloneHTML(result, title) {
|
|
38
|
+
const pageTitle = result.title || title;
|
|
39
|
+
const dataJson = JSON.stringify(result).replace(/</g, '\\u003c');
|
|
40
|
+
const safeTitle = String(pageTitle)
|
|
41
|
+
.replace(/&/g, '&')
|
|
42
|
+
.replace(/</g, '<')
|
|
43
|
+
.replace(/>/g, '>')
|
|
44
|
+
.replace(/"/g, '"')
|
|
45
|
+
.replace(/'/g, ''');
|
|
46
|
+
return `<!DOCTYPE html>
|
|
47
|
+
<html lang="en">
|
|
48
|
+
<head>
|
|
49
|
+
<meta charset="UTF-8">
|
|
50
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
51
|
+
<title>${safeTitle} — Summary</title>
|
|
52
|
+
<style>
|
|
53
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
54
|
+
body {
|
|
55
|
+
font-family: Georgia, 'Times New Roman', serif;
|
|
56
|
+
background: #1a1a2e;
|
|
57
|
+
color: #e0e0e0;
|
|
58
|
+
min-height: 100vh;
|
|
59
|
+
display: flex;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
padding: 40px 20px;
|
|
62
|
+
line-height: 1.7;
|
|
63
|
+
}
|
|
64
|
+
#sheet { max-width: 720px; width: 100%; }
|
|
65
|
+
h1 { font-size: 28px; color: #a78bfa; margin-bottom: 16px; }
|
|
66
|
+
#overview { font-size: 18px; color: #cfcfe6; margin-bottom: 28px; }
|
|
67
|
+
h2 { font-size: 18px; color: #a78bfa; margin: 28px 0 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
68
|
+
ol { list-style: none; counter-reset: pt; }
|
|
69
|
+
ol li {
|
|
70
|
+
counter-increment: pt;
|
|
71
|
+
position: relative;
|
|
72
|
+
padding: 10px 0 10px 44px;
|
|
73
|
+
border-bottom: 1px solid #2a2a44;
|
|
74
|
+
font-size: 17px;
|
|
75
|
+
}
|
|
76
|
+
ol li::before {
|
|
77
|
+
content: counter(pt);
|
|
78
|
+
position: absolute;
|
|
79
|
+
left: 0;
|
|
80
|
+
top: 10px;
|
|
81
|
+
width: 28px;
|
|
82
|
+
height: 28px;
|
|
83
|
+
background: #252547;
|
|
84
|
+
border: 1px solid #3a3a5c;
|
|
85
|
+
border-radius: 50%;
|
|
86
|
+
color: #a78bfa;
|
|
87
|
+
font-size: 14px;
|
|
88
|
+
font-weight: 700;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
}
|
|
93
|
+
dl { display: grid; grid-template-columns: max-content 1fr; gap: 8px 16px; }
|
|
94
|
+
dt { color: #c4b5fd; font-weight: 700; font-size: 16px; }
|
|
95
|
+
dd { font-size: 16px; color: #cfcfe6; }
|
|
96
|
+
.meta { text-align: center; color: #555; font-size: 12px; margin-top: 40px; }
|
|
97
|
+
</style>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<div id="sheet">
|
|
101
|
+
<h1 id="title"></h1>
|
|
102
|
+
<p id="overview"></p>
|
|
103
|
+
<h2 id="points-h" style="display:none">Key Points</h2>
|
|
104
|
+
<ol id="points"></ol>
|
|
105
|
+
<h2 id="glossary-h" style="display:none">Glossary</h2>
|
|
106
|
+
<dl id="glossary"></dl>
|
|
107
|
+
</div>
|
|
108
|
+
<script>
|
|
109
|
+
(function () {
|
|
110
|
+
var s = ${dataJson};
|
|
111
|
+
document.getElementById('title').textContent = s.title || 'Summary';
|
|
112
|
+
if (s.overview) document.getElementById('overview').textContent = s.overview;
|
|
113
|
+
var ol = document.getElementById('points');
|
|
114
|
+
var points = s.keyPoints || [];
|
|
115
|
+
if (points.length) {
|
|
116
|
+
document.getElementById('points-h').style.display = '';
|
|
117
|
+
points.forEach(function (p) {
|
|
118
|
+
var li = document.createElement('li');
|
|
119
|
+
li.textContent = p;
|
|
120
|
+
ol.appendChild(li);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
var dl = document.getElementById('glossary');
|
|
124
|
+
var glossary = s.glossary || [];
|
|
125
|
+
if (glossary.length) {
|
|
126
|
+
document.getElementById('glossary-h').style.display = '';
|
|
127
|
+
glossary.forEach(function (g) {
|
|
128
|
+
var dt = document.createElement('dt');
|
|
129
|
+
dt.textContent = g.term;
|
|
130
|
+
var dd = document.createElement('dd');
|
|
131
|
+
dd.textContent = g.definition;
|
|
132
|
+
dl.appendChild(dt);
|
|
133
|
+
dl.appendChild(dd);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
var meta = document.createElement('div');
|
|
137
|
+
meta.className = 'meta';
|
|
138
|
+
meta.textContent = 'Created with Playable Lessons';
|
|
139
|
+
document.getElementById('sheet').appendChild(meta);
|
|
140
|
+
})();
|
|
141
|
+
</script>
|
|
142
|
+
</body>
|
|
143
|
+
</html>`;
|
|
144
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.APP_VERSION = void 0;
|
|
4
|
+
// AUTO-GENERATED by scripts/embed-version.mjs — do not edit by hand.
|
|
5
|
+
// Source: package.json (version). Regenerate: npm run embed:version
|
|
6
|
+
/* eslint-disable */
|
|
7
|
+
exports.APP_VERSION = "0.6.2";
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playable-lessons",
|
|
3
|
-
"version": "0.2
|
|
4
|
-
"description": "Interactive Fiction Authoring Tool for Education — Generate playable branching stories from source material using AI + Ink",
|
|
3
|
+
"version": "0.6.2",
|
|
5
4
|
"main": "./out/main/index.mjs",
|
|
6
5
|
"scripts": {
|
|
7
6
|
"dev": "electron-vite dev",
|
|
@@ -16,11 +15,13 @@
|
|
|
16
15
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json",
|
|
17
16
|
"typecheck:web": "tsc --noEmit -p tsconfig.web.json",
|
|
18
17
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
|
19
|
-
"lint": "eslint .
|
|
18
|
+
"lint": "eslint .",
|
|
20
19
|
"test": "vitest run",
|
|
21
20
|
"test:watch": "vitest",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
21
|
+
"embed:runtime": "node scripts/embed-ink-runtime.mjs",
|
|
22
|
+
"embed:version": "node scripts/embed-version.mjs",
|
|
23
|
+
"build:cli": "node scripts/embed-version.mjs && tsc -p tsconfig.cli.json",
|
|
24
|
+
"prepublishOnly": "npm run embed:runtime && npm run build:cli",
|
|
24
25
|
"cli": "npx tsx src/cli/index.ts"
|
|
25
26
|
},
|
|
26
27
|
"bin": {
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"yargs": "^18.0.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
59
|
+
"@electron/notarize": "^2.5.0",
|
|
58
60
|
"@types/node": "^25.5.0",
|
|
59
61
|
"@types/react": "^19.2.14",
|
|
60
62
|
"@types/react-dom": "^19.2.3",
|
|
@@ -67,6 +69,8 @@
|
|
|
67
69
|
"electron-builder": "^26.8.1",
|
|
68
70
|
"electron-vite": "^5.0.0",
|
|
69
71
|
"eslint": "^10.1.0",
|
|
72
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
73
|
+
"express": "^5.2.1",
|
|
70
74
|
"react": "^19.2.4",
|
|
71
75
|
"react-dom": "^19.2.4",
|
|
72
76
|
"tsx": "^4.21.0",
|