playable-lessons 0.2.2 → 0.6.3

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