@lernapps/app-templates 0.0.1 → 0.1.1
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 +22 -0
- package/README.md +195 -2
- package/dist/appname-Bh23cUuz.mjs +12 -0
- package/dist/assessment-Dy359O1O.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-CxtCPAa5.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-CteIFN96.mjs +254 -0
- package/dist/content-DO6oEJxj.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-CU0n_unV.mjs +89 -0
- package/dist/engine-CArxqizW.mjs +136 -0
- package/dist/explainer-e2e.mjs +1311 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2132 -0
- package/dist/explainer.mjs +1246 -0
- package/dist/html-mffkHGHh.mjs +89 -0
- package/dist/image-CspJVfyQ.mjs +21 -0
- package/dist/interactive-e2e.mjs +140 -0
- package/dist/interactive-plugin.mjs +242 -0
- package/dist/interactive.mjs +75 -0
- package/dist/load-RosvaPZf.mjs +16976 -0
- package/dist/needs-BzEahJ6X.mjs +97 -0
- package/dist/pages-9RaaomoO.mjs +81 -0
- package/dist/questions-e2e.mjs +1 -0
- package/dist/questions-engine.mjs +3 -0
- package/dist/questions.mjs +6 -0
- package/dist/quiz-DnW-xtCh.mjs +70 -0
- package/dist/quiz-e2e.mjs +174 -0
- package/dist/quiz-plugin.mjs +266 -0
- package/dist/quiz.mjs +246 -0
- package/dist/render-D5kwkNt1.mjs +507 -0
- package/dist/render-DqHOJyHK.mjs +146 -0
- package/dist/render-GS1Htlu8.mjs +545 -0
- package/dist/result-D_mmCVwN.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-BDwL7Su3.mjs +1076 -0
- package/dist/validate-Bxy9NfB8.mjs +146 -0
- package/dist/vue-DO1m6q1R.mjs +40 -0
- package/dist/vue-code-BkJjW_I_.mjs +320 -0
- package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
- package/docs/arc42/02-constraints.arc42.md +228 -0
- package/docs/arc42/03-system-context.arc42.md +118 -0
- package/docs/arc42/04-solution-strategy.arc42.md +47 -0
- package/docs/arc42/05-building-blocks.arc42.md +307 -0
- package/docs/arc42/06-runtime-view.arc42.md +98 -0
- package/docs/arc42/07-deployment-view.arc42.md +160 -0
- package/docs/arc42/08-concepts.arc42.md +123 -0
- package/docs/arc42/09-decisions.arc42.md +246 -0
- package/docs/arc42/10-quality-requirements.arc42.md +155 -0
- package/docs/arc42/11-risks.arc42.md +113 -0
- package/docs/arc42/12-glossary.arc42.md +162 -0
- package/docs/arc42/architecture-evidence.md +395 -0
- package/explainer/.arc42ignore +3 -0
- package/explainer/README.md +54 -0
- package/explainer/_gitignore +5 -0
- package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
- package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
- package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
- package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
- package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
- package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
- package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
- package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
- package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
- package/explainer/e2e/explainer.e2e.ts +3 -0
- package/explainer/index.html +11 -0
- package/explainer/package.json +18 -0
- package/explainer/playwright.config.ts +1 -0
- package/explainer/src/app.yaml +20 -0
- package/explainer/src/components/ProzentFeld.test.ts +30 -0
- package/explainer/src/components/ProzentFeld.ts +121 -0
- package/explainer/src/components/ProzentFeld.vue +84 -0
- package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
- package/explainer/src/main.ts +4 -0
- package/explainer/src/prozent/chapter.yaml +18 -0
- package/explainer/src/prozent/grundwert.yaml +115 -0
- package/explainer/src/prozent/prozentsatz.yaml +99 -0
- package/explainer/src/prozent/prozentwert.yaml +119 -0
- package/explainer/tsconfig.json +1 -0
- package/explainer/vite.config.ts +4 -0
- package/interactive/.arc42ignore +5 -0
- package/interactive/README.md +28 -0
- package/interactive/_gitignore +4 -0
- package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
- package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
- package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
- package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
- package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
- package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
- package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
- package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
- package/interactive/e2e/interactive.e2e.ts +3 -0
- package/interactive/index.html +11 -0
- package/interactive/package.json +18 -0
- package/interactive/playwright.config.ts +1 -0
- package/interactive/src/app.yaml +28 -0
- package/interactive/src/core/index.ts +101 -0
- package/interactive/src/data/lessons.json +4 -0
- package/interactive/src/env.d.ts +6 -0
- package/interactive/src/main.ts +6 -0
- package/interactive/src/messages/de.json +12 -0
- package/interactive/src/playthroughs/grundreihe.yaml +33 -0
- package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
- package/interactive/src/ui/App.vue +85 -0
- package/interactive/tsconfig.json +1 -0
- package/interactive/vite.config.ts +4 -0
- package/package.json +143 -4
- package/quiz/README.md +31 -0
- package/quiz/_gitignore +5 -0
- package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
- package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
- package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
- package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
- package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
- package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
- package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
- package/quiz/e2e/quiz.e2e.ts +3 -0
- package/quiz/index.html +11 -0
- package/quiz/package.json +18 -0
- package/quiz/playwright.config.ts +1 -0
- package/quiz/src/images/berge-ordnen.jpg +0 -0
- package/quiz/src/images/everest-hoehe.jpg +0 -0
- package/quiz/src/images/galapagos.jpg +0 -0
- package/quiz/src/images/grand-canyon.jpg +0 -0
- package/quiz/src/images/totes-meer.jpg +0 -0
- package/quiz/src/images/uluru.jpg +0 -0
- package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
- package/quiz/src/main.ts +4 -0
- package/quiz/src/quiz.json +312 -0
- package/quiz/tsconfig.json +1 -0
- package/quiz/vite.config.ts +4 -0
- package/runtime/explainer/assessment-client.ts +478 -0
- package/runtime/explainer/assessment-page.ts +448 -0
- package/runtime/explainer/assessment.ts +393 -0
- package/runtime/explainer/check-page.ts +520 -0
- package/runtime/explainer/components/exercise.ts +409 -0
- package/runtime/explainer/components/props.ts +230 -0
- package/runtime/explainer/components/read.ts +428 -0
- package/runtime/explainer/content.ts +277 -0
- package/runtime/explainer/e2e.ts +1488 -0
- package/runtime/explainer/exercises.ts +326 -0
- package/runtime/explainer/explainer.css +1283 -0
- package/runtime/explainer/explainer.ts +838 -0
- package/runtime/explainer/explainer.v1.schema.json +1165 -0
- package/runtime/explainer/expression.ts +407 -0
- package/runtime/explainer/fraction.ts +107 -0
- package/runtime/explainer/generated/comparator.ts +31 -0
- package/runtime/explainer/generated/question.ts +165 -0
- package/runtime/explainer/generated/soundness.ts +280 -0
- package/runtime/explainer/generated/texts.ts +257 -0
- package/runtime/explainer/generated/types.ts +103 -0
- package/runtime/explainer/generated/values.ts +136 -0
- package/runtime/explainer/islands.ts +281 -0
- package/runtime/explainer/load.ts +457 -0
- package/runtime/explainer/messages/de.json +200 -0
- package/runtime/explainer/messages/en.json +200 -0
- package/runtime/explainer/messages.ts +13 -0
- package/runtime/explainer/needs.ts +122 -0
- package/runtime/explainer/open-task.ts +235 -0
- package/runtime/explainer/outline.ts +306 -0
- package/runtime/explainer/plugin.ts +488 -0
- package/runtime/explainer/position.ts +62 -0
- package/runtime/explainer/prompt.ts +75 -0
- package/runtime/explainer/render.ts +686 -0
- package/runtime/explainer/result.ts +238 -0
- package/runtime/explainer/routes.ts +119 -0
- package/runtime/explainer/schema.ts +140 -0
- package/runtime/explainer/scope.ts +87 -0
- package/runtime/explainer/stuck.ts +57 -0
- package/runtime/explainer/tasks.ts +109 -0
- package/runtime/explainer/tutor.ts +469 -0
- package/runtime/explainer/tutorpage.ts +669 -0
- package/runtime/explainer/video.ts +123 -0
- package/runtime/interactive/contract.ts +121 -0
- package/runtime/interactive/e2e.ts +190 -0
- package/runtime/interactive/interactive.css +82 -0
- package/runtime/interactive/interactive.ts +115 -0
- package/runtime/interactive/interactive.v1.schema.json +146 -0
- package/runtime/interactive/load.ts +188 -0
- package/runtime/interactive/messages/de.json +14 -0
- package/runtime/interactive/messages/en.json +14 -0
- package/runtime/interactive/messages.ts +11 -0
- package/runtime/interactive/playthrough.v1.schema.json +143 -0
- package/runtime/interactive/plugin.ts +266 -0
- package/runtime/interactive/render.ts +126 -0
- package/runtime/quiz/e2e.ts +233 -0
- package/runtime/quiz/engine.ts +242 -0
- package/runtime/quiz/messages/de.json +23 -0
- package/runtime/quiz/messages/en.json +23 -0
- package/runtime/quiz/plugin.ts +150 -0
- package/runtime/quiz/quiz.css +154 -0
- package/runtime/quiz/quiz.ts +56 -0
- package/runtime/quiz/quiz.v1.schema.json +72 -0
- package/runtime/quiz/render.ts +173 -0
- package/runtime/quiz/validate.ts +108 -0
- package/runtime/shared/appname.ts +16 -0
- package/runtime/shared/blocks/block.ts +162 -0
- package/runtime/shared/blocks/blocks.css +190 -0
- package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
- package/runtime/shared/blocks/code.ts +26 -0
- package/runtime/shared/blocks/figures.ts +49 -0
- package/runtime/shared/blocks/image.ts +25 -0
- package/runtime/shared/blocks/index.ts +13 -0
- package/runtime/shared/blocks/messages/de.json +4 -0
- package/runtime/shared/blocks/messages/en.json +4 -0
- package/runtime/shared/blocks/render.ts +134 -0
- package/runtime/shared/blocks/validate.ts +165 -0
- package/runtime/shared/content.ts +197 -0
- package/runtime/shared/examples.ts +20 -0
- package/runtime/shared/html.ts +10 -0
- package/runtime/shared/languages.ts +94 -0
- package/runtime/shared/page.css +101 -0
- package/runtime/shared/pages.ts +114 -0
- package/runtime/shared/questions/e2e.ts +115 -0
- package/runtime/shared/questions/engine.ts +209 -0
- package/runtime/shared/questions/index.ts +39 -0
- package/runtime/shared/questions/messages/de.json +26 -0
- package/runtime/shared/questions/messages/en.json +26 -0
- package/runtime/shared/questions/question.ts +433 -0
- package/runtime/shared/questions/question.v1.schema.json +346 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +255 -0
- package/runtime/shared/questions/validate.ts +518 -0
- package/runtime/shared/schema.ts +44 -0
- package/runtime/shared/text/check.ts +68 -0
- package/runtime/shared/text/formula.ts +69 -0
- package/runtime/shared/text/markdown.ts +185 -0
- package/runtime/shared/text/speech.ts +39 -0
- package/runtime/shared/text/text.v1.schema.json +34 -0
- package/runtime/shared/vue-code.ts +395 -0
- package/runtime/shared/vue.ts +38 -0
|
@@ -0,0 +1,2132 @@
|
|
|
1
|
+
import { _ as exampleProblems, a as routes, c as exerciseData, d as templatesOf, g as describedProblems, h as TABLE_CONTRACT, i as hiddenChapter, l as renderExercise, m as SIZE_BUDGET, n as loadSite, o as terms, p as formulaAlternative, r as contentPages, s as dataOf, t as ContentError, u as taskSeeds, v as propertyProblems, y as propsIn } from "./load-RosvaPZf.mjs";
|
|
2
|
+
import { i as inLanguage, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
|
|
3
|
+
import { n as markerOf } from "./block-BK_ohsH1.mjs";
|
|
4
|
+
import { n as renderMaterial } from "./blocks-CxtCPAa5.mjs";
|
|
5
|
+
import { c as renderMarkdown, l as renderWithFormulas, o as plain, s as renderInline } from "./check-CteIFN96.mjs";
|
|
6
|
+
import { _ as questionIn, a as renderQuestion, i as questionMessagesIn, n as answerText } from "./render-GS1Htlu8.mjs";
|
|
7
|
+
import { n as isSizeless, t as imageType } from "./image-CspJVfyQ.mjs";
|
|
8
|
+
import { a as pageFooter, c as withLanguage, i as notFoundPage, n as fromDepth, o as pageHeader, r as languageRoot, s as relative, t as alternateLinks } from "./pages-9RaaomoO.mjs";
|
|
9
|
+
import { B as label, D as generate, F as questionFor, H as placeholdersIn, S as videoLine, U as plainText, V as placeholderValue, X as kindOf, _ as termId, a as assessments, b as renderVideo, h as allBlocks, m as titleOf, o as drawsTasks, p as stepHint, r as assessmentOf, v as timelineEvents, w as message } from "./assessment-Dy359O1O.mjs";
|
|
10
|
+
import { i as NEW_TASK_ICON } from "./tasks-DADUYy52.mjs";
|
|
11
|
+
import { n as NEEDS, o as languageName, t as LANGUAGES } from "./needs-BzEahJ6X.mjs";
|
|
12
|
+
import { n as vueAlias, r as withServer, t as VUE_OPTIONS } from "./vue-DO1m6q1R.mjs";
|
|
13
|
+
import { Marked } from "marked";
|
|
14
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { gzipSync } from "node:zlib";
|
|
18
|
+
// runtime/explainer/tutor.ts
|
|
19
|
+
const WORDS$3 = {
|
|
20
|
+
de: {
|
|
21
|
+
task: "Aufgabe",
|
|
22
|
+
answerFormat: "Antwortformat",
|
|
23
|
+
solution: "Lösung",
|
|
24
|
+
explanation: "Erklärung",
|
|
25
|
+
hint: "Tipp der App",
|
|
26
|
+
mistakes: "Typische Fehler und was die App sagt",
|
|
27
|
+
componentMistakes: "Typische Fehler (die Meldung der App passt zu einem davon)",
|
|
28
|
+
options: "Antworten zur Wahl (✓ richtig, ✗ falsch, mit der Meldung der App)",
|
|
29
|
+
right: "✓",
|
|
30
|
+
wrong: "✗",
|
|
31
|
+
component: "interaktive Übung",
|
|
32
|
+
does: "Was zu tun ist",
|
|
33
|
+
correct: "Was als richtig zählt",
|
|
34
|
+
example: "Beispiel",
|
|
35
|
+
note: "Merke",
|
|
36
|
+
figure: "Bild",
|
|
37
|
+
caption: "Bildunterschrift",
|
|
38
|
+
labels: "Beschriftungen",
|
|
39
|
+
interactive: "Interaktiv",
|
|
40
|
+
video: "Video",
|
|
41
|
+
exerciseHere: "Übung `{id}` (unten)",
|
|
42
|
+
points: "Eine gute Antwort enthält (P1, P2 … wie die App sie nennt)",
|
|
43
|
+
pointContent: "Inhalt",
|
|
44
|
+
pointForm: "Form",
|
|
45
|
+
source: "Quelle",
|
|
46
|
+
modelAnswer: "Musterantwort",
|
|
47
|
+
answerWays: {
|
|
48
|
+
text: "in eigenen Worten eintippen",
|
|
49
|
+
photo: "in eigenen Worten auf Papier schreiben und das Blatt fotografieren",
|
|
50
|
+
both: "in eigenen Worten eintippen oder auf Papier schreiben und das Blatt fotografieren"
|
|
51
|
+
},
|
|
52
|
+
openGuide: "So hilfst du: Die App zeigt die Punkte erst nach der Antwort; das Kind hakt selbst ab, welche seine Antwort enthält, und „es fehlen P2, P4“ nennt die anderen. Fehlt dir die Antwort, bitte das Kind, sie einzufügen oder das Foto seines Blatts anzuhängen. Prüf Punkt für Punkt, auch die abgehakten, und sag zuerst, was gelungen ist. Zu einem fehlenden Punkt stell eine Frage, die dorthin führt, und nenn seine Quelle. Verrate die Musterantwort nicht, bevor das Kind es noch einmal versucht hat. Zu einem Punkt der Form zeig an einem Satz des Kindes, wie es besser geht.",
|
|
53
|
+
formats: {
|
|
54
|
+
"single-choice": "eine Antwort wählen",
|
|
55
|
+
"multiple-choice": "alle richtigen Antworten wählen",
|
|
56
|
+
"true-false": "„Stimmt“ oder „Stimmt nicht“ wählen",
|
|
57
|
+
number: "eine Zahl eintippen",
|
|
58
|
+
ordering: "die Einträge in die richtige Reihenfolge bringen (die App zeigt sie gemischt)",
|
|
59
|
+
matching: "jedem Eintrag links den passenden rechts zuordnen",
|
|
60
|
+
choice: "eine Antwort wählen",
|
|
61
|
+
set: "alle richtigen Antworten wählen",
|
|
62
|
+
order: "die Einträge in die richtige Reihenfolge bringen (die App zeigt sie gemischt)",
|
|
63
|
+
pairs: "jedem Eintrag links den passenden rechts zuordnen"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
en: {
|
|
67
|
+
task: "Task",
|
|
68
|
+
answerFormat: "Answer format",
|
|
69
|
+
solution: "Solution",
|
|
70
|
+
explanation: "Explanation",
|
|
71
|
+
hint: "The app's hint",
|
|
72
|
+
mistakes: "Typical mistakes and what the app says",
|
|
73
|
+
componentMistakes: "Typical mistakes (the app's message fits one of them)",
|
|
74
|
+
options: "Choices (✓ right, ✗ wrong, with the app's message)",
|
|
75
|
+
right: "✓",
|
|
76
|
+
wrong: "✗",
|
|
77
|
+
component: "interactive exercise",
|
|
78
|
+
does: "What to do",
|
|
79
|
+
correct: "What counts as right",
|
|
80
|
+
example: "Example",
|
|
81
|
+
note: "Remember",
|
|
82
|
+
figure: "Picture",
|
|
83
|
+
caption: "Caption",
|
|
84
|
+
labels: "Labels",
|
|
85
|
+
interactive: "Interactive",
|
|
86
|
+
video: "Video",
|
|
87
|
+
exerciseHere: "Exercise `{id}` (below)",
|
|
88
|
+
points: "A good answer has (P1, P2 … as the app names them)",
|
|
89
|
+
pointContent: "content",
|
|
90
|
+
pointForm: "form",
|
|
91
|
+
source: "Source",
|
|
92
|
+
modelAnswer: "Model answer",
|
|
93
|
+
answerWays: {
|
|
94
|
+
text: "type it in their own words",
|
|
95
|
+
photo: "write it in their own words on paper and take a photo of the sheet",
|
|
96
|
+
both: "type it in their own words, or write it on paper and take a photo of the sheet"
|
|
97
|
+
},
|
|
98
|
+
openGuide: "How to help: the app shows the points only after the answer; the child ticks which ones the answer has, and “missing P2, P4” names the others. If you lack the answer, ask the child to paste it or to attach the photo of the sheet. Check point by point, the ticked ones too, and say first what went well. For a missing point, ask a question that leads to it and name its source. Do not give away the model answer before the child has tried again. For a point of form, show with one of the child's sentences how it gets better.",
|
|
99
|
+
formats: {
|
|
100
|
+
"single-choice": "choose one answer",
|
|
101
|
+
"multiple-choice": "choose all right answers",
|
|
102
|
+
"true-false": "choose “true” or “false”",
|
|
103
|
+
number: "type a number",
|
|
104
|
+
ordering: "put the items in the right order (the app shows them shuffled)",
|
|
105
|
+
matching: "match each item on the left with one on the right",
|
|
106
|
+
choice: "choose one answer",
|
|
107
|
+
set: "choose all right answers",
|
|
108
|
+
order: "put the items in the right order (the app shows them shuffled)",
|
|
109
|
+
pairs: "match each item on the left with one on the right"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const wordsIn$2 = (lang) => WORDS$3[lang] ?? WORDS$3["de"];
|
|
114
|
+
/** The words of what a tutor reads in `lang`, which the tutor page shares (tutorpage.ts). */
|
|
115
|
+
const tutorWords = (lang) => wordsIn$2(lang);
|
|
116
|
+
/** A text on one line, as a list item holds it. */
|
|
117
|
+
const line$1 = (text) => text.replace(/\s+/g, " ").trim();
|
|
118
|
+
/** The folder of a page's tutor page, in the page's folder: <chapter>/<page>/tutor/ (tutorpage.ts). */
|
|
119
|
+
const TUTOR_PAGE = "tutor";
|
|
120
|
+
/** The address of a page's tutor page within its language tree. */
|
|
121
|
+
const tutorPageOf = (page) => `${page.chapter}/${page.id}/${TUTOR_PAGE}/`;
|
|
122
|
+
/** The answer format of a question of the question types, in `lang`. */
|
|
123
|
+
function questionFormat(question, words) {
|
|
124
|
+
const format = words.formats[question.type] ?? "";
|
|
125
|
+
return question.type === "number" && question.unit !== void 0 ? `${format} (${question.unit})` : format;
|
|
126
|
+
}
|
|
127
|
+
/** A question of the question types, in `lang`: its text, format, options, solution, explanation, hint, mistakes. */
|
|
128
|
+
function questionPart(question, lang, words) {
|
|
129
|
+
const lines = [`- ${words.task}: ${line$1(question.text)}`, `- ${words.answerFormat}: ${questionFormat(question, words)}`];
|
|
130
|
+
if (question.type === "single-choice" || question.type === "multiple-choice") {
|
|
131
|
+
lines.push(`- ${words.options}:`);
|
|
132
|
+
for (const option of question.options) {
|
|
133
|
+
const info = option.info.trim() === "" ? "" : ` – ${line$1(option.info)}`;
|
|
134
|
+
lines.push(` - ${option.correct ? words.right : words.wrong} ${line$1(option.text)}${info}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
lines.push(`- ${words.solution}: ${line$1(plainText(answerText(question, lang)))}`);
|
|
138
|
+
if (question.explanation.trim() !== "") lines.push(`- ${words.explanation}: ${line$1(question.explanation)}`);
|
|
139
|
+
if (question.hint !== void 0) lines.push(`- ${words.hint}: ${line$1(question.hint)}`);
|
|
140
|
+
if (question.type === "number" && (question.mistakes ?? []).length > 0) {
|
|
141
|
+
lines.push(`- ${words.mistakes}:`);
|
|
142
|
+
for (const mistake of question.mistakes ?? []) lines.push(` - ${mistake.answer}: ${line$1(mistake.message)}`);
|
|
143
|
+
}
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
146
|
+
/** A generated exercise's answer format, in `lang`; the tutor page gives the rest (tutorpage.ts). */
|
|
147
|
+
function generatedPart(spec, lang, words) {
|
|
148
|
+
const kind = kindOf(spec.answer.compare);
|
|
149
|
+
const unit = kind.kind === "number" && kind.unit !== void 0 ? ` (${inLanguage(kind.unit, lang)})` : "";
|
|
150
|
+
return [`- ${words.answerFormat}: ${words.formats[kind.kind === "number" ? "number" : kind.kind]}${unit}`];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* An open task in `lang`: the task, how the learner answers, the points of a good answer as P1, P2 … (each with its
|
|
154
|
+
* kind and, for content, a link to its source from the tutor page, `base` up to the language tree), the model answer,
|
|
155
|
+
* the hint (their Markdown as written, formulas $…$ kept), and how to help.
|
|
156
|
+
*/
|
|
157
|
+
function openTaskPart(block, lang, words, base) {
|
|
158
|
+
const ways = block.answer ?? ["text", "photo"];
|
|
159
|
+
const way = ways.includes("text") && ways.includes("photo") ? "both" : ways.includes("photo") ? "photo" : "text";
|
|
160
|
+
const lines = [
|
|
161
|
+
`- ${words.task}: ${line$1(inLanguage(block.task, lang))}`,
|
|
162
|
+
`- ${words.answerFormat}: ${words.answerWays[way]}`,
|
|
163
|
+
`- ${words.points}:`,
|
|
164
|
+
...block.points.map((point, index) => {
|
|
165
|
+
const kind = point.kind === "form" ? words.pointForm : words.pointContent;
|
|
166
|
+
const source = point.source === void 0 ? "" : ` ${words.source}: [${point.source}](${base}${point.source.slice(1)})`;
|
|
167
|
+
return ` - P${index + 1} (${kind}) ${line$1(inLanguage(point.text, lang))}${source}`;
|
|
168
|
+
}),
|
|
169
|
+
`- ${words.modelAnswer}: ${line$1(inLanguage(block.solution, lang))}`
|
|
170
|
+
];
|
|
171
|
+
if (block.hint !== void 0) lines.push(`- ${words.hint}: ${line$1(inLanguage(block.hint, lang))}`);
|
|
172
|
+
lines.push(`- ${words.openGuide}`);
|
|
173
|
+
return lines;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* An exercise component: what it says about itself (its about), its typical mistakes as F1, F2 …, which the app's
|
|
177
|
+
* message of a wrong answer names one of, and when it declares its exercise also its task once as its template; F1, F2 …
|
|
178
|
+
* are then the columns of its table of tasks. Its tasks by number are the tutor page's.
|
|
179
|
+
*/
|
|
180
|
+
function componentPart(description, lang, words) {
|
|
181
|
+
const { about } = description;
|
|
182
|
+
const exercise = description.exercise?.[lang];
|
|
183
|
+
const t = (text) => line$1(inLanguage(text, lang));
|
|
184
|
+
const lines = [`- ${words.does}: ${t(about.task)}`];
|
|
185
|
+
if (exercise !== void 0) lines.push(`- ${words.task}: „${line$1(exercise.template)}“`);
|
|
186
|
+
lines.push(`- ${words.answerFormat}: ${t(about.answer)}`, `- ${words.correct}: ${t(about.correct)}`);
|
|
187
|
+
if (about.mistakes.length > 0) lines.push(`- ${words.componentMistakes}:`, ...about.mistakes.map((mistake, index) => ` - F${index + 1}: ${t(mistake)}`));
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
190
|
+
/** A text of restricted Markdown for a tutor: each link as its words, as its address is the page's (an absolute one stays). */
|
|
191
|
+
const unlinked = (text) => text.replace(/\[([^\]]*)\]\(([^)\s]+)\)/g, (whole, words, target) => /^[a-z][a-z0-9+.-]*:/i.test(target) ? whole : words);
|
|
192
|
+
/**
|
|
193
|
+
* What a tutor reads of `blocks` in `lang`, as Markdown lines, the content of a page for its tutor page (tutorpage.ts):
|
|
194
|
+
* each section as the heading `heading` makes,
|
|
195
|
+
* then its content: texts, formulas as $…$, definitions, examples and notes with their kind and title, figures by their
|
|
196
|
+
* alt text and caption, tables row by row, timelines, quotes, code, a video as a link with its title and channel, a
|
|
197
|
+
* component's own nojs text; an exercise by its
|
|
198
|
+
* id, as it follows in full after the page. `inline` turns a text into what the page shows. A text keeps its lines,
|
|
199
|
+
* so that its lists stay lists.
|
|
200
|
+
*/
|
|
201
|
+
function contentLines(site, blocks, lang, inline, heading) {
|
|
202
|
+
const words = wordsIn$2(lang);
|
|
203
|
+
const t = (text) => inline(unlinked(inLanguage(text, lang).trim()));
|
|
204
|
+
const one = (text) => t(text).replace(/\s+/g, " ");
|
|
205
|
+
const sentence = (text) => one(text).replace(/[.!?]?$/, (end) => end || ".");
|
|
206
|
+
const lines = [];
|
|
207
|
+
const add = (...more) => lines.push(...more, "");
|
|
208
|
+
const walk = (inner, anchor, depth) => {
|
|
209
|
+
for (const block of inner) switch (block.type) {
|
|
210
|
+
case "section": {
|
|
211
|
+
const id = [...anchor, block.id];
|
|
212
|
+
add(heading(one(block.title), id.join("/"), depth));
|
|
213
|
+
walk(block.blocks, id, depth + 1);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case "example":
|
|
217
|
+
case "note":
|
|
218
|
+
add(`**${block.type === "example" ? words.example : words.note}: ${one(block.title)}**`);
|
|
219
|
+
walk(block.blocks, anchor, depth);
|
|
220
|
+
break;
|
|
221
|
+
case "text":
|
|
222
|
+
add(t(block.text));
|
|
223
|
+
break;
|
|
224
|
+
case "formula":
|
|
225
|
+
add(inline(`$${inLanguage(block.formula, lang).replace(/\s+/g, " ").trim()}$`));
|
|
226
|
+
break;
|
|
227
|
+
case "definition":
|
|
228
|
+
add(`- **${one(block.term)}**: ${one(block.explanation)}`);
|
|
229
|
+
break;
|
|
230
|
+
case "figure":
|
|
231
|
+
add(`${words.figure}: ${sentence(block.alt)} ${words.caption}: ${one(block.caption)}`);
|
|
232
|
+
break;
|
|
233
|
+
case "labelled-figure": {
|
|
234
|
+
const labels = block.labels.map((label, index) => `${markerOf(label, index)} ${one(label.text)}`).join(", ");
|
|
235
|
+
const caption = block.caption === void 0 ? "" : ` ${words.caption}: ${one(block.caption)}`;
|
|
236
|
+
add(`${words.figure}: ${sentence(block.alt)}${caption} ${words.labels}: ${labels}`);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "table":
|
|
240
|
+
add(`${one(block.caption)}:`, ...[...block.header === void 0 ? [] : [block.header], ...block.rows].map((row) => `- ${row.map(one).join(" | ")}`));
|
|
241
|
+
break;
|
|
242
|
+
case "timeline":
|
|
243
|
+
add(...timelineEvents(site, block).map((event) => {
|
|
244
|
+
const label = event.label === void 0 ? "" : ` (${one(event.label)})`;
|
|
245
|
+
const text = event.text === void 0 ? "" : ` – ${one(event.text)}`;
|
|
246
|
+
return `- ${String(event.date)}${label}: ${one(event.title)}${text}`;
|
|
247
|
+
}));
|
|
248
|
+
break;
|
|
249
|
+
case "quote": {
|
|
250
|
+
const { origin } = block;
|
|
251
|
+
const from = [
|
|
252
|
+
origin.author,
|
|
253
|
+
origin.work,
|
|
254
|
+
origin.year
|
|
255
|
+
].filter((part) => part !== void 0).map((part) => typeof part === "number" ? String(part) : one(part));
|
|
256
|
+
add(...t(block.text).split("\n").map((text) => `> ${text}`), ">", `> – ${from.join(", ")}`);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case "code":
|
|
260
|
+
add(...block.caption === void 0 ? [] : [`${one(block.caption)}:`, ""], `\`\`\`${block.language}`, block.code.trimEnd(), "```");
|
|
261
|
+
break;
|
|
262
|
+
case "video":
|
|
263
|
+
add(videoLine(block, words.video));
|
|
264
|
+
break;
|
|
265
|
+
case "vue-component": {
|
|
266
|
+
const use = site.components.find((candidate) => candidate.block === block);
|
|
267
|
+
if (use?.component.exercise === true && use.id !== void 0) add(`- ${words.exerciseHere.replace("{id}", use.id)}`);
|
|
268
|
+
else if (block.nojs !== void 0) add(`${words.interactive}: ${one(block.nojs)}`);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
default: if ("id" in block && typeof block.id === "string") add(`- ${words.exerciseHere.replace("{id}", block.id)}`);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
walk(blocks, [], 0);
|
|
275
|
+
return lines;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* An exercise for a tutor in `lang`, or undefined for a block that is none: its id, its type, and its lines (task,
|
|
279
|
+
* answer format, solution, hint, typical mistakes; of a generated exercise and an exercise component what stays the
|
|
280
|
+
* same for each number, the tutor page adds the tasks by number).
|
|
281
|
+
*/
|
|
282
|
+
function exercisePart(site, block, lang, descriptions) {
|
|
283
|
+
const words = wordsIn$2(lang);
|
|
284
|
+
switch (block.type) {
|
|
285
|
+
case "generated": return {
|
|
286
|
+
id: block.id,
|
|
287
|
+
type: `generated, ${kindOf(block.answer.compare).kind}`,
|
|
288
|
+
lines: generatedPart(block, lang, words)
|
|
289
|
+
};
|
|
290
|
+
case "open-task": return {
|
|
291
|
+
id: block.id,
|
|
292
|
+
type: "open-task",
|
|
293
|
+
lines: openTaskPart(block, lang, words, "../../../")
|
|
294
|
+
};
|
|
295
|
+
case "vue-component": {
|
|
296
|
+
const use = site.components.find((candidate) => candidate.block === block);
|
|
297
|
+
const description = descriptions.get(block);
|
|
298
|
+
if (use?.id === void 0 || description === void 0) return void 0;
|
|
299
|
+
return {
|
|
300
|
+
id: use.id,
|
|
301
|
+
type: `${words.component} ${block.component}`,
|
|
302
|
+
lines: componentPart(description, lang, words)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
case "single-choice":
|
|
306
|
+
case "multiple-choice":
|
|
307
|
+
case "true-false":
|
|
308
|
+
case "number":
|
|
309
|
+
case "ordering":
|
|
310
|
+
case "matching": return {
|
|
311
|
+
id: block.id,
|
|
312
|
+
type: block.type,
|
|
313
|
+
lines: questionPart(questionIn(block, lang), lang, words)
|
|
314
|
+
};
|
|
315
|
+
default: return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** The anchors of a page: its sections (nested as <section>/<subsection>), exercises and components with an id. */
|
|
319
|
+
function anchorsOf(site, page) {
|
|
320
|
+
const anchors = /* @__PURE__ */ new Set();
|
|
321
|
+
const walk = (blocks, anchor) => {
|
|
322
|
+
for (const block of blocks) if (block.type === "section") {
|
|
323
|
+
anchors.add([...anchor, block.id].join("/"));
|
|
324
|
+
walk(block.blocks, [...anchor, block.id]);
|
|
325
|
+
} else if (block.type === "example" || block.type === "note") walk(block.blocks, anchor);
|
|
326
|
+
};
|
|
327
|
+
walk(page.blocks, []);
|
|
328
|
+
for (const block of allBlocks(page.blocks)) if ("id" in block && typeof block.id === "string" && block.type !== "section") anchors.add(block.id);
|
|
329
|
+
for (const use of site.components) if (use.id !== void 0 && [...allBlocks(page.blocks)].includes(use.block)) anchors.add(use.id);
|
|
330
|
+
return anchors;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The links of a Markdown file at `file` (an address within a language tree, e.g. "prozent/grundwert/tutor/") that
|
|
334
|
+
* lead nowhere: a relative link must name a file the build writes or a page's
|
|
335
|
+
* folder, and its anchor must be on that page. `files` are the files and page folders of the tree, `anchors` each
|
|
336
|
+
* page folder's anchors. One line per broken link.
|
|
337
|
+
*/
|
|
338
|
+
function linkProblems(file, text, files, anchors) {
|
|
339
|
+
const problems = [];
|
|
340
|
+
const base = new URL(file, "https://app.invalid/");
|
|
341
|
+
for (const [, target = ""] of text.matchAll(/\]\(([^)\s]+)\)/g)) {
|
|
342
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue;
|
|
343
|
+
const url = new URL(target, base);
|
|
344
|
+
const path = decodeURIComponent(url.pathname.slice(1));
|
|
345
|
+
if (!files.has(path)) {
|
|
346
|
+
problems.push(`${file}: the link ${target} leads to ${path || "./"}, which the build does not write`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const id = decodeURIComponent(url.hash.slice(1));
|
|
350
|
+
if (id !== "" && !(anchors.get(path)?.has(id) ?? false)) problems.push(`${file}: the link ${target} names the anchor #${id}, which ${path || "./"} does not have`);
|
|
351
|
+
}
|
|
352
|
+
return problems;
|
|
353
|
+
}
|
|
354
|
+
// runtime/explainer/prompt.ts
|
|
355
|
+
/** Where the start prompt's address starts: replaced by the address of the language tree when the page uses it. */
|
|
356
|
+
const BASE = "⟦base⟧";
|
|
357
|
+
/** Where the start prompt of a content page has the learner's position: replaced by the query the page builds. */
|
|
358
|
+
const QUERY = "⟦query⟧";
|
|
359
|
+
/** The assistants a learner can switch on, the same for every app: each takes the prompt in its address. */
|
|
360
|
+
const PROVIDERS = [
|
|
361
|
+
{
|
|
362
|
+
name: "Claude",
|
|
363
|
+
url: "https://claude.ai/new?q={prompt}"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: "ChatGPT",
|
|
367
|
+
url: "https://chatgpt.com/?q={prompt}"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: "Mistral Vibe",
|
|
371
|
+
url: "https://chat.mistral.ai/chat?q={prompt}",
|
|
372
|
+
paste: true
|
|
373
|
+
}
|
|
374
|
+
];
|
|
375
|
+
const MARK = "lernapps-prompt";
|
|
376
|
+
/** A provider's address without the prompt, for the link without JavaScript: its parameter with {prompt} left out. */
|
|
377
|
+
function plainAddress(url) {
|
|
378
|
+
const parsed = new URL(url.replace(/\{prompt\}/g, MARK));
|
|
379
|
+
const keys = Array.from(parsed.searchParams.entries()).flatMap(([key, value]) => value.includes(MARK) ? [key] : []);
|
|
380
|
+
for (const key of keys) parsed.searchParams.delete(key);
|
|
381
|
+
return parsed.href.replace(/\?$/, "");
|
|
382
|
+
}
|
|
383
|
+
const LOAD = {
|
|
384
|
+
de: "Lade {url} und sei mein Tutor.",
|
|
385
|
+
en: "Load {url} and be my tutor."
|
|
386
|
+
};
|
|
387
|
+
const PLACEHOLDER = {
|
|
388
|
+
de: "Ich brauche Hilfe bei einer Lern-Einheit. Ich schreibe Dir gleich, worum es geht.",
|
|
389
|
+
en: "I need help with a learning unit. I will tell you in a moment what it is about."
|
|
390
|
+
};
|
|
391
|
+
/** The short message a `paste` assistant gets in its address, in `lang`: it holds no address to fetch. */
|
|
392
|
+
const placeholderPrompt = (lang) => PLACEHOLDER[lang] ?? PLACEHOLDER["de"] ?? "";
|
|
393
|
+
/**
|
|
394
|
+
* The start prompt a provider link carries from a content page, the one kind of page that starts a tutor, in its
|
|
395
|
+
* language: "Lade <chapter>/<page>/tutor/<QUERY> und sei mein Tutor.", the page's tutor page with
|
|
396
|
+
* where the learner is in its query, which the client module fills in (position.ts, pageQuery). Its address starts
|
|
397
|
+
* with BASE.
|
|
398
|
+
*/
|
|
399
|
+
function startPrompt(route) {
|
|
400
|
+
const page = {
|
|
401
|
+
chapter: route.page?.chapter ?? "",
|
|
402
|
+
id: route.page?.id ?? ""
|
|
403
|
+
};
|
|
404
|
+
return (LOAD[route.lang] ?? LOAD["de"] ?? "").replace("{url}", `${BASE}${tutorPageOf(page)}${QUERY}`);
|
|
405
|
+
}
|
|
406
|
+
// runtime/explainer/tutorpage.ts
|
|
407
|
+
/** The name of a tutor page's Markdown, beside the page in its folder: for an assistant that fetches nothing. */
|
|
408
|
+
const TUTOR_MARKDOWN = "index.md";
|
|
409
|
+
/** The size a tutor page stays within, in bytes: an exercise component gets as many numbers as fit; the build notes a larger page. */
|
|
410
|
+
const TUTOR_PAGE_BUDGET = 64e3;
|
|
411
|
+
/** The size of a text in bytes, as UTF-8. */
|
|
412
|
+
const bytesOf = (text) => new TextEncoder().encode(text).length;
|
|
413
|
+
const WORDS$2 = {
|
|
414
|
+
de: {
|
|
415
|
+
title: "{page} – für deinen KI-Tutor",
|
|
416
|
+
tutorPage: "Tutor-Seite",
|
|
417
|
+
forTutor: "Was ein KI-Tutor zur Seite „{page}“ bekommt.",
|
|
418
|
+
intro: "Du hilfst einem Kind beim Lernen. Es arbeitet mit der Lern-App „{app}“ von lernapps.net, gerade auf der Seite „{page}“, und steckt dort in einer Übung. Gelöst wird in der App, sie prüft die Antworten. Du erklärst.",
|
|
419
|
+
parents: "Eltern und Lehrkräfte sehen hier, was der Tutor bekommt.",
|
|
420
|
+
toPage: "Zur Seite",
|
|
421
|
+
scope: "Der Lernumfang dieser Seite, mit dem, was dazugehört und was nicht: [Scope](../scope/). Lies ihn, bevor du eine offene Antwort beurteilst; liegt die Quelle eines Punkts auf einer anderen Seite, führt er zum Scope des Kapitels. Seine volle Adresse ist die dieser Tutor-Seite mit `scope/` statt `tutor/`. Konntest du ihn nicht abrufen, etwa wegen Sicherheitseinstellungen, nenn dem Kind genau diese volle Adresse und bitte es, sie freizugeben; ruf sie danach noch einmal ab. Bis dahin beurteilst du keine offene Antwort und antwortest nichts zum Inhalt aus Allgemeinwissen.",
|
|
422
|
+
guideTitle: "So leitest du an",
|
|
423
|
+
guide: [
|
|
424
|
+
"Antworte kurz: höchstens drei Sätze und eine Frage. Keine Überschriften, keine Listen.",
|
|
425
|
+
"Nimm die Beispiele und Aufgaben dieser Seite. Erfinde keine eigenen. Rechne nichts selbst aus: Lösungen und Fehler stehen unten bei jeder Nummer.",
|
|
426
|
+
"Im Chat erklärst du, am Beispiel der Seite. Gerechnet wird in der App: Die Aufgaben löst das Kind dort. Rechnet es eine Aufgabe im Chat, schick ihm sofort den Link zu ihr.",
|
|
427
|
+
"Verrate keine Lösung. Ein Wert, der in der Tabelle einer Übung „(verdeckt)“ heißt, steht nicht im Text der Aufgabe: Das Kind sieht ihn nicht, nenn ihn nicht.",
|
|
428
|
+
"Was das Kind in der App tun kann, steht bei der Übung unter „Was zu tun ist“ und „Antwortformat“. Beschreib keine anderen Knöpfe oder Schritte.",
|
|
429
|
+
"Bitte das Kind nie, eine Aufgabe abzuschreiben. Du kennst sie aus dem Kontext unten."
|
|
430
|
+
],
|
|
431
|
+
startTitle: "So beginnst du",
|
|
432
|
+
start: ["„Sei mein Tutor“: Frag zuerst, was das Kind über das Thema schon weiß. Dann geh die Seite der Reihe nach durch, ein Abschnitt pro Nachricht: erst warum das Thema wichtig ist, dann die Regel, dann das Beispiel. Prüf nach jedem Abschnitt mit einer kurzen Frage zum Inhalt, ob das Kind ihn verstanden hat; ein „Ja“ reicht nicht. Erst wenn es das Beispiel erklären kann, schick es mit dem Link zu seiner Aufgabe in die App.", "„Hilf mir bei Übung …, Nr. …“ (das Kind kommt mit „Hilfe“ aus der App): Schlag Übung und Nummer unten nach und setz bei dieser Aufgabe an. Steht da „Die App sagt: …“, such den Fehler (F1, F2 …) der Übung, zu dem die Meldung passt: Das ist der Fehler des Kindes. Nennt die Nachricht eine Eingabe und steht sie bei dieser Nummer in einer Spalte F…, bestätigt das den Fehler. Sag in einem Satz, was an genau dieser Eingabe nicht zur Aufgabe passt, und wiederhol die Meldung nicht. Fehlt beides, frag, was das Kind eingegeben oder gebaut hat und wo es hängt. Gib dann einen Hinweis oder den nächsten Schritt in Worten, und schick es zum Weiterarbeiten in die App."],
|
|
433
|
+
appRules: "Regeln dieser App",
|
|
434
|
+
whereTitle: "Wo das Kind gerade ist",
|
|
435
|
+
where: [
|
|
436
|
+
"Die erste Nachricht des Kindes nennt diese Seite und je Übung die Nummer der Aufgabe, die das Kind gerade vor sich hat, z. B. `?{example}`. Schlag die Nummern unten bei den Übungen nach. Kommt das Kind mit „Hilfe“, nennt `uebung=` die Übung und `abschnitt=` ihren Abschnitt.",
|
|
437
|
+
"Steht in der Nachricht „Die App sagt: …“, hat die App das zur Eingabe des Kindes gemeldet. Setz dort an, ohne es zu wiederholen.",
|
|
438
|
+
"Eine Zeile wie „…, Nr. 16: richtig im 2. Versuch.“ kommt von „Zurück“ in der App: Übung, Nummer und wie weit das Kind damit kam.",
|
|
439
|
+
"„Ich habe auf … geübt:“ kommt von „Zurück zum Tutor“: diese Seite mit den Nummern, dann je geübter Aufgabe, wie es lief, bei einer falschen mit Eingabe und Meldung der App. Setz bei den falschen an, eine nach der anderen.",
|
|
440
|
+
"Jede Aufgabe hat einen Link: die Adresse der Seite, also dieser Tutor-Seite ohne `tutor/` und ohne `?…`, mit der Nummer und `via=tutor`, z. B. `{link}`. Schick ihn immer als volle Adresse, mit Schema und Host dieser Tutor-Seite (z. B. `https://`). Für eine weitere Aufgabe nimm eine andere Nummer der Übung und nenn ihren Text. Sag dazu: „Löse sie in der App und drück dann ‚Zurück‘. Füg die Zeile hier ein.“"
|
|
441
|
+
],
|
|
442
|
+
needsTitle: "Besondere Anforderungen",
|
|
443
|
+
needsIntro: "Nennt die Adresse dieser Seite `bedarf=`, gilt jeder Abschnitt, den sie nennt, mehrere durch Komma getrennt. Er geht dem oben Gesagten vor; widersprechen sich zwei, gilt der strengere. Ohne `bedarf=` gilt keiner.",
|
|
444
|
+
needs: {
|
|
445
|
+
"kurze-schritte": "Im Gegensatz zu oben: höchstens zwei kurze Sätze, ein Gedanke pro Nachricht, jeder Teil eines Abschnitts mit eigener Frage. Sag, wie weit es noch ist.",
|
|
446
|
+
"wenig-lesen": "Im Gegensatz zu oben: höchstens zwei kurze Sätze ohne Nebensätze; sag den Kern eines Abschnitts selbst, statt ihn lesen zu lassen. Erklär jedes Fachwort einmal, korrigier keine Rechtschreibung.",
|
|
447
|
+
"mehr-tiefe": "Hak sichere Abschnitte mit einer Prüffrage ab. Im Gegensatz zu oben darfst du an den Beispielen der Seite Warum- und Was-wäre-wenn-Fragen stellen; erfinde aber keine Rechenaufgaben.",
|
|
448
|
+
"andere-muttersprache": "Antworte in einfachem Deutsch; nenn ein neues Fachwort mit Artikel, Mehrzahl und, wenn du sie sicher kennst, der Übersetzung in Klammern. Versteht das Kind zweimal nicht, erklär einen Satz in seiner Sprache.",
|
|
449
|
+
"klar-und-woertlich": "Schreib wörtlich: keine Ironie, keine Redewendungen. Im Gegensatz zu oben fragst du am Anfang mit zwei oder drei Antworten zur Auswahl und nennst immer „Abschnitt x von y“.",
|
|
450
|
+
"rechnen-anschaulich": "Zeig Zahlen der Seite als Menge, z. B. „37 sind 3 Zehner und 7 Einer“, aber nie das Ergebnis einer Aufgabe. Ein Schritt pro Nachricht, nie Zeitdruck."
|
|
451
|
+
},
|
|
452
|
+
contextTitle: "Kontext: die Seite „{page}“",
|
|
453
|
+
exercise: "Übung",
|
|
454
|
+
steps: "Lösungsweg",
|
|
455
|
+
link: "Link",
|
|
456
|
+
linkNumber: "Link zu Nummer n",
|
|
457
|
+
table: "Aufgaben nach Nummer: die Werte für `{…}`, die Lösung und was bei den Fehlern herauskommt. Ein Fehler ohne Spalte hat keinen festen Wert; die Meldung der App sagt, ob er vorliegt.",
|
|
458
|
+
hidden: "(verdeckt)",
|
|
459
|
+
answerIs: "`{answer}` ist die Lösung.",
|
|
460
|
+
number: "Nr.",
|
|
461
|
+
choices: "Zur Wahl",
|
|
462
|
+
tasks: "Aufgaben nach Nummer, mit Lösung",
|
|
463
|
+
shown: "Hier stehen die Nummern 1 bis {shown} von {count}. Sitzt das Kind an einer anderen Nummer, schick ihm den Link zu einer Nummer von hier.",
|
|
464
|
+
none: "Die Nummern dieser Übung passen nicht auf diese Seite. Was das Kind dort tut, erfährst du aus seinen Zeilen von „Hilfe“ und „Zurück“."
|
|
465
|
+
},
|
|
466
|
+
en: {
|
|
467
|
+
title: "{page} – for your AI tutor",
|
|
468
|
+
tutorPage: "tutor page",
|
|
469
|
+
forTutor: "What an AI tutor gets for the page “{page}”.",
|
|
470
|
+
intro: "You help a child learn. It works with the learning app “{app}” of lernapps.net, right now on the page “{page}”, and is in an exercise there. Tasks are solved in the app, which checks the answers. You explain.",
|
|
471
|
+
parents: "Parents and teachers see here what the tutor gets.",
|
|
472
|
+
toPage: "To the page",
|
|
473
|
+
scope: "The learning path of this page, with what belongs to it and what does not: [scope](../scope/). Read it before you judge an open answer; if the source of a point is on another page, it leads to the chapter's scope. Its full address is the one of this tutor page with `scope/` in place of `tutor/`. If you could not fetch it, for example because of security settings, give the child exactly this full address and ask them to allow it; then fetch it again. Until then, judge no open answer and answer nothing about the content from general knowledge.",
|
|
474
|
+
guideTitle: "How you guide",
|
|
475
|
+
guide: [
|
|
476
|
+
"Answer briefly: three sentences and one question at most. No headings, no lists.",
|
|
477
|
+
"Use the examples and tasks of this page. Do not make up your own. Work nothing out yourself: solutions and mistakes are below with each number.",
|
|
478
|
+
"In the chat you explain, with the page's example. Tasks are worked out in the app: the child solves them there. If it works a task out in the chat, send it the link to the task at once.",
|
|
479
|
+
"Do not give a solution away. A value the table of an exercise calls “(hidden)” is not in the task's text: the child does not see it, do not name it.",
|
|
480
|
+
"What the child can do in the app is under “What to do” and “Answer format” with the exercise. Do not describe other buttons or steps.",
|
|
481
|
+
"Never ask the child to copy a task. You know it from the context below."
|
|
482
|
+
],
|
|
483
|
+
startTitle: "How you start",
|
|
484
|
+
start: ["“Be my tutor”: first ask what the child already knows about the topic. Then go through the page in order, one section per message: first why the topic matters, then the rule, then the example. After each section check with a short question on its content that the child understood it; a “yes” is not enough. Only once it can explain the example, send it to the app with the link to its task.", "“Help me with exercise …, no. …” (the child comes from “Help” in the app): look the exercise and the number up below and start right at this task. If it says “The app says: …”, find the mistake (F1, F2 …) of the exercise that the message fits: that is the child's mistake. If the message names an input and it is in a column F… for this number, that confirms the mistake. Say in one sentence what about exactly this input does not fit the task, and do not repeat the message. If both are missing, ask what the child entered or built and where it is stuck. Then give a hint or the next step in words, and send it back to the app to go on."],
|
|
485
|
+
appRules: "Rules of this app",
|
|
486
|
+
whereTitle: "Where the child is",
|
|
487
|
+
where: [
|
|
488
|
+
"The child's first message names this page and, for each exercise, the number of the task the child has in front of it, e.g. `?{example}`. Look the numbers up below with the exercises. If the child comes with “Help”, `uebung=` names the exercise and `abschnitt=` its section.",
|
|
489
|
+
"If the message says “The app says: …”, the app showed that for the child's input. Build on it without repeating it.",
|
|
490
|
+
"A line like “…, no. 16: right on try 2.” comes from “Back” in the app: the exercise, the number and how far the child got with it.",
|
|
491
|
+
"“I practised on …:” comes from “Back to the tutor”: this page with the numbers, then for each task practised how it went, a wrong one with the input and the app's message. Start with the wrong ones, one after the other.",
|
|
492
|
+
"Every task has a link: the address of the page, that is of this tutor page without `tutor/` and without `?…`, with the number and `via=tutor`, e.g. `{link}`. Always send it as a full address, with the scheme and host of this tutor page (e.g. `https://`). For a further task take another number of the exercise and say its text. Add: “Solve it in the app, then press ‘Back’. Paste the line here.”"
|
|
493
|
+
],
|
|
494
|
+
needsTitle: "Special requirements",
|
|
495
|
+
needsIntro: "If the address of this page names `bedarf=`, each section it names holds, several separated by commas. It goes before what is said above; if two contradict each other, the stricter one holds. Without `bedarf=` none holds.",
|
|
496
|
+
needs: {
|
|
497
|
+
"kurze-schritte": "Unlike above: two short sentences at most, one thought per message, each part of a section with a question of its own. Say how far there is still to go.",
|
|
498
|
+
"wenig-lesen": "Unlike above: two short sentences at most, without subordinate clauses; say the core of a section yourself instead of having it read. Explain every technical term once, do not correct spelling.",
|
|
499
|
+
"mehr-tiefe": "Tick off sections the child knows with one checking question. Unlike above you may ask why and what-if questions about the page's examples; but do not make up tasks to work out.",
|
|
500
|
+
"andere-muttersprache": "Answer in simple language, in the language of this page; give a new technical term with its article and plural, if any, and, if you know it for sure, its translation in brackets. If the child does not understand twice, explain one sentence in its language.",
|
|
501
|
+
"klar-und-woertlich": "Write literally: no irony, no idioms. Unlike above, at the start you ask with two or three answers to choose from, and you always say “section x of y”.",
|
|
502
|
+
"rechnen-anschaulich": "Show numbers of the page as amounts, e.g. “37 is 3 tens and 7 ones”, but never the result of a task. One step per message, never time pressure."
|
|
503
|
+
},
|
|
504
|
+
contextTitle: "Context: the page “{page}”",
|
|
505
|
+
exercise: "Exercise",
|
|
506
|
+
steps: "Steps of the solution",
|
|
507
|
+
link: "Link",
|
|
508
|
+
linkNumber: "Link to number n",
|
|
509
|
+
table: "Tasks by number: the values for `{…}`, the solution and what the mistakes give. A mistake without a column has no fixed value; the app's message says whether it is there.",
|
|
510
|
+
hidden: "(hidden)",
|
|
511
|
+
answerIs: "`{answer}` is the solution.",
|
|
512
|
+
number: "No.",
|
|
513
|
+
choices: "Choices",
|
|
514
|
+
tasks: "Tasks by number, with solution",
|
|
515
|
+
shown: "Here are the numbers 1 to {shown} of {count}. If the child is at another number, send it the link to a number from here.",
|
|
516
|
+
none: "The numbers of this exercise do not fit on this page. What the child does there you learn from its lines of “Help” and “Back”."
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const wordsIn$1 = (lang) => WORDS$2[lang] ?? WORDS$2["de"];
|
|
520
|
+
/** A text on one line, as a list item holds it, for the page's Markdown (formulas). */
|
|
521
|
+
const line = (text) => formulas(text.replace(/\s+/g, " ").trim());
|
|
522
|
+
/**
|
|
523
|
+
* A text for the page's Markdown, its lines kept: no HTML in it, and each formula $…$ on one line, which the page's
|
|
524
|
+
* Markdown keeps as written (FORMULA).
|
|
525
|
+
*/
|
|
526
|
+
const formulas = (text) => text.split(/(\$[^$]+\$)/).map((part, index) => index % 2 === 1 ? part.replace(/\s+/g, " ") : part.replace(/</g, "<")).join("");
|
|
527
|
+
/** The types of the question blocks, exercises without numbers. */
|
|
528
|
+
const QUESTIONS = /* @__PURE__ */ new Set([
|
|
529
|
+
"single-choice",
|
|
530
|
+
"multiple-choice",
|
|
531
|
+
"true-false",
|
|
532
|
+
"number",
|
|
533
|
+
"ordering",
|
|
534
|
+
"matching"
|
|
535
|
+
]);
|
|
536
|
+
/** The exercises of `page` in order, each with its section and the number of its numbered tasks. */
|
|
537
|
+
function pageExercises(site, page, descriptions) {
|
|
538
|
+
const found = [];
|
|
539
|
+
const walk = (blocks, anchor, title) => {
|
|
540
|
+
for (const block of blocks) if (block.type === "section") walk(block.blocks, [...anchor, block.id], block.title);
|
|
541
|
+
else if (block.type === "example" || block.type === "note") walk(block.blocks, anchor, title);
|
|
542
|
+
else {
|
|
543
|
+
const where = anchor.length === 0 ? {} : {
|
|
544
|
+
section: anchor.join("/"),
|
|
545
|
+
...title === void 0 ? {} : { sectionTitle: title }
|
|
546
|
+
};
|
|
547
|
+
if (block.type === "generated") found.push({
|
|
548
|
+
id: block.id,
|
|
549
|
+
block,
|
|
550
|
+
...where,
|
|
551
|
+
count: taskSeeds(block, dataOf(block, site.data)).length
|
|
552
|
+
});
|
|
553
|
+
else if (block.type === "vue-component") {
|
|
554
|
+
const use = site.components.find((candidate) => candidate.block === block);
|
|
555
|
+
const description = descriptions.get(block);
|
|
556
|
+
if (use?.id !== void 0 && use.component.exercise) found.push({
|
|
557
|
+
id: use.id,
|
|
558
|
+
block,
|
|
559
|
+
...where,
|
|
560
|
+
count: description?.seeds.length ?? 0
|
|
561
|
+
});
|
|
562
|
+
} else if (block.type === "open-task") found.push({
|
|
563
|
+
id: block.id,
|
|
564
|
+
block,
|
|
565
|
+
...where,
|
|
566
|
+
count: 0
|
|
567
|
+
});
|
|
568
|
+
else if (QUESTIONS.has(block.type) && "id" in block && typeof block.id === "string") found.push({
|
|
569
|
+
id: block.id,
|
|
570
|
+
block,
|
|
571
|
+
...where,
|
|
572
|
+
count: 0
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
walk(page.blocks, [], void 0);
|
|
577
|
+
return found;
|
|
578
|
+
}
|
|
579
|
+
/** The anchors of the sections of `page`, nested as <section>/<subsection>. */
|
|
580
|
+
function sectionsOf(page) {
|
|
581
|
+
const walk = (blocks, anchor) => blocks.flatMap((block) => block.type === "section" ? [[...anchor, block.id].join("/"), ...walk(block.blocks, [...anchor, block.id])] : []);
|
|
582
|
+
return walk(page.blocks, []);
|
|
583
|
+
}
|
|
584
|
+
/** A template of the content with each placeholder {{x}} as {x}, on one line, for the page's Markdown. */
|
|
585
|
+
const template = (text, lang) => line(inLanguage(text, lang).replace(/\{\{\s*([\w.]+)\s*\}\}/g, "{$1}"));
|
|
586
|
+
/** The placeholders of a generated exercise's texts in `lang`, in order and each once, but {{answer}}, the solution. */
|
|
587
|
+
function placeholdersOf(spec, lang) {
|
|
588
|
+
const texts = [
|
|
589
|
+
spec.task.text,
|
|
590
|
+
...spec.hint === void 0 ? [] : [spec.hint],
|
|
591
|
+
...spec.steps ?? [],
|
|
592
|
+
...(spec.mistakes ?? []).map((mistake) => mistake.message)
|
|
593
|
+
];
|
|
594
|
+
const all = [...new Set(texts.flatMap((text) => placeholdersIn(inLanguage(text, lang))))];
|
|
595
|
+
return {
|
|
596
|
+
paths: all.filter((path) => path !== "answer"),
|
|
597
|
+
answer: all.includes("answer")
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
/** A cell of a table of tasks: on one line, without the table's separator. */
|
|
601
|
+
const cell = (text) => text.replace(/\s+/g, " ").replace(/\|/g, "/").trim() || "–";
|
|
602
|
+
/**
|
|
603
|
+
* A generated exercise for the tutor, once: its task as a template ({G} for the value of G), its answer format, hint
|
|
604
|
+
* and steps, its typical mistakes as F1, F2 … with the app's message, and the link to a number; then a table of each
|
|
605
|
+
* task number's inputs and outputs: the values of the placeholders, the choices of a choice, the solution and what
|
|
606
|
+
* each mistake gives, all as the learner reads them, so that the tutor fills in and works nothing out. The examples
|
|
607
|
+
* stay out: they are test cases. The link leads to the exercise's page at `page`, from where the tutor page is.
|
|
608
|
+
*/
|
|
609
|
+
function generatedLines(spec, site, lang, format, page = "../") {
|
|
610
|
+
const words = wordsIn$1(lang);
|
|
611
|
+
const shared = tutorWords(lang);
|
|
612
|
+
const lines = [`- ${shared.task}: „${template(spec.task.text, lang)}“`, ...format];
|
|
613
|
+
if (spec.hint !== void 0) lines.push(`- ${shared.hint}: „${template(spec.hint, lang)}“`);
|
|
614
|
+
if (spec.steps !== void 0) lines.push(`- ${words.steps}:`, ...spec.steps.map((step) => ` - ${template(step, lang)}`));
|
|
615
|
+
const mistakes = spec.mistakes ?? [];
|
|
616
|
+
if (mistakes.length > 0) lines.push(`- ${shared.mistakes}:`, ...mistakes.map((mistake, index) => ` - F${index + 1}: „${template(mistake.message, lang)}“`));
|
|
617
|
+
lines.push(`- ${words.linkNumber}: \`${page}?${spec.id}=n&via=tutor#${spec.id}\``);
|
|
618
|
+
const { paths, answer } = placeholdersOf(spec, lang);
|
|
619
|
+
const data = dataOf(spec, site.data);
|
|
620
|
+
const templates = templatesOf(spec, [lang]);
|
|
621
|
+
let choices = false;
|
|
622
|
+
const rows = taskSeeds(spec, data).map((seed, index) => {
|
|
623
|
+
const task = generate(spec, seed, data);
|
|
624
|
+
const question = questionFor(spec, task, lang, templates);
|
|
625
|
+
const options = question.type === "single-choice" || question.type === "multiple-choice" ? [cell(question.options.map((option) => plainText(option.text)).join("; "))] : [];
|
|
626
|
+
if (options.length > 0) choices = true;
|
|
627
|
+
const wrong = mistakes.map((_mistake, at) => {
|
|
628
|
+
try {
|
|
629
|
+
return cell(label(task.mistakes[at] ?? "", lang));
|
|
630
|
+
} catch {
|
|
631
|
+
return "–";
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
return [
|
|
635
|
+
String(index + 1),
|
|
636
|
+
...paths.map((path) => cell(placeholderValue(spec, task, path, lang)?.text ?? "")),
|
|
637
|
+
...options,
|
|
638
|
+
cell(plainText(answerText(question, lang))),
|
|
639
|
+
...wrong
|
|
640
|
+
].join(" | ");
|
|
641
|
+
});
|
|
642
|
+
const header = [
|
|
643
|
+
words.number,
|
|
644
|
+
...paths,
|
|
645
|
+
...choices ? [words.choices] : [],
|
|
646
|
+
shared.solution,
|
|
647
|
+
...mistakes.map((_mistake, index) => `F${index + 1}`)
|
|
648
|
+
].join(" | ");
|
|
649
|
+
lines.push(`- ${words.table}${answer ? ` ${words.answerIs}` : ""}`, "", "```", header, ...rows, "```");
|
|
650
|
+
return lines;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* The table of tasks of an exercise component, as a generated exercise has it: each task number's inputs, its
|
|
654
|
+
* solution and what each typical mistake gives, all as the learner reads them ("–" for a mistake without one answer).
|
|
655
|
+
*/
|
|
656
|
+
function componentTable(exercise, lang) {
|
|
657
|
+
const words = wordsIn$1(lang);
|
|
658
|
+
const count = exercise.tasks[0]?.mistakes.length ?? 0;
|
|
659
|
+
const columns = Array.from({ length: count }, (_mistake, index) => index).filter((index) => exercise.tasks.some((row) => (row.mistakes[index] ?? "") !== ""));
|
|
660
|
+
const shown = new Set([...exercise.template.matchAll(/\{([^{}\s]+)\}/g)].map((match) => match[1]));
|
|
661
|
+
const header = [
|
|
662
|
+
words.number,
|
|
663
|
+
...exercise.inputs.map((input) => shown.has(input) ? input : `${input} ${words.hidden}`),
|
|
664
|
+
tutorWords(lang).solution,
|
|
665
|
+
...columns.map((index) => `F${index + 1}`)
|
|
666
|
+
].join(" | ");
|
|
667
|
+
const rows = exercise.tasks.map((row, index) => [
|
|
668
|
+
String(index + 1),
|
|
669
|
+
...row.inputs,
|
|
670
|
+
row.solution,
|
|
671
|
+
...columns.map((column) => row.mistakes[column] ?? "")
|
|
672
|
+
].map(cell).join(" | "));
|
|
673
|
+
return [
|
|
674
|
+
`- ${words.table}`,
|
|
675
|
+
"",
|
|
676
|
+
"```",
|
|
677
|
+
header,
|
|
678
|
+
...rows,
|
|
679
|
+
"```"
|
|
680
|
+
];
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* The hint of the build for the exercise components that declare no table of tasks (TABLE_CONTRACT), each by its
|
|
684
|
+
* place, or none: their tutor pages have their tasks only as text from describe, as many as fit. A hint for now;
|
|
685
|
+
* rule vue-component-tutor-table.
|
|
686
|
+
*/
|
|
687
|
+
function tableHint(site, descriptions) {
|
|
688
|
+
const without = site.components.filter((use) => {
|
|
689
|
+
const description = descriptions.get(use.block);
|
|
690
|
+
return description !== void 0 && description.exercise === void 0;
|
|
691
|
+
});
|
|
692
|
+
if (without.length === 0) return void 0;
|
|
693
|
+
const one = without.length === 1;
|
|
694
|
+
return `lernapps explainer: ${without.length} exercise component${one ? " declares" : "s declare"} no table of tasks, so ${one ? "its tutor page has its" : "their tutor pages have their"} tasks only as text, as many as fit; export ${TABLE_CONTRACT} from ${one ? "its" : "their"} logic module: ${without.map((use) => `${use.place}: ${use.component.name}`).join(", ")} (vue-component-tutor-table)`;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* The tutor page of `page` in `lang`, as Markdown, its links relative to the page's folder <chapter>/<page>/tutor/,
|
|
698
|
+
* which the build checks: who the tutor helps, how to guide, where the child is, the app's own rules; then the page's
|
|
699
|
+
* content as context, and its exercises. An exercise component shows its table of tasks with every number, or without
|
|
700
|
+
* one each number's task and solution; when the page would grow beyond `budget`, the components without a table share
|
|
701
|
+
* what is left, number by number in turn, and `cut` names those with numbers left out.
|
|
702
|
+
*/
|
|
703
|
+
function tutorPage(site, page, lang, descriptions, budget = TUTOR_PAGE_BUDGET) {
|
|
704
|
+
const words = wordsIn$1(lang);
|
|
705
|
+
const exercises = pageExercises(site, page, descriptions);
|
|
706
|
+
const rows = /* @__PURE__ */ new Map();
|
|
707
|
+
for (const exercise of exercises) {
|
|
708
|
+
if (exercise.block.type !== "vue-component" || exercise.count === 0) continue;
|
|
709
|
+
if (descriptions.get(exercise.block)?.exercise?.[lang] !== void 0) continue;
|
|
710
|
+
const tasks = descriptions.get(exercise.block)?.tasks[lang] ?? [];
|
|
711
|
+
rows.set(exercise.id, tasks.map((task) => ` - ${words.number} ${task.number}: ${line(task.task)} → ${tutorWords(lang).solution}: ${line(task.solution)}`));
|
|
712
|
+
}
|
|
713
|
+
const shown = /* @__PURE__ */ new Map();
|
|
714
|
+
const build = () => pageMarkdown(site, page, lang, descriptions, exercises, rows, shown);
|
|
715
|
+
const size = (text) => bytesOf(tutorPageDocument(text, site, page, lang));
|
|
716
|
+
let markdown = build();
|
|
717
|
+
if (size(markdown) > budget && rows.size > 0) {
|
|
718
|
+
for (const id of rows.keys()) shown.set(id, 0);
|
|
719
|
+
let room = budget - size(build());
|
|
720
|
+
const cost = (row) => bytesOf(escape(row)) + 10;
|
|
721
|
+
for (let more = true; more;) {
|
|
722
|
+
more = false;
|
|
723
|
+
for (const [id, all] of rows) {
|
|
724
|
+
const next = all[shown.get(id) ?? 0];
|
|
725
|
+
if (next === void 0 || cost(next) > room) continue;
|
|
726
|
+
room -= cost(next);
|
|
727
|
+
shown.set(id, (shown.get(id) ?? 0) + 1);
|
|
728
|
+
more = true;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
markdown = build();
|
|
732
|
+
while (size(markdown) > budget) {
|
|
733
|
+
const [largest] = [...shown.entries()].sort((a, b) => b[1] - a[1]);
|
|
734
|
+
if (largest === void 0 || largest[1] === 0) break;
|
|
735
|
+
shown.set(largest[0], largest[1] - 1);
|
|
736
|
+
markdown = build();
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const cut = [...rows].flatMap(([id, all]) => {
|
|
740
|
+
const n = shown.get(id) ?? all.length;
|
|
741
|
+
return n < all.length ? [{
|
|
742
|
+
id,
|
|
743
|
+
shown: n,
|
|
744
|
+
count: all.length
|
|
745
|
+
}] : [];
|
|
746
|
+
});
|
|
747
|
+
return {
|
|
748
|
+
markdown,
|
|
749
|
+
cut
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* The learner's needs on a tutor page, in `lang`, after its guide (dec-tutor-needs): what holds when the query names
|
|
754
|
+
* them in `bedarf=`, then a short section for each, as Markdown lines.
|
|
755
|
+
*/
|
|
756
|
+
function needsLines(lang) {
|
|
757
|
+
const words = wordsIn$1(lang);
|
|
758
|
+
return [
|
|
759
|
+
`## ${words.needsTitle}`,
|
|
760
|
+
"",
|
|
761
|
+
words.needsIntro,
|
|
762
|
+
"",
|
|
763
|
+
...NEEDS.map((need) => `- \`${need}\`: ${words.needs[need]}`),
|
|
764
|
+
""
|
|
765
|
+
];
|
|
766
|
+
}
|
|
767
|
+
/** The Markdown of a tutor page, with of each exercise component the rows by number `shown` names (all if none). */
|
|
768
|
+
function pageMarkdown(site, page, lang, descriptions, exercises, rows, shown) {
|
|
769
|
+
const words = wordsIn$1(lang);
|
|
770
|
+
const own = (text) => line(inLanguage(text, lang));
|
|
771
|
+
const title = own(page.title);
|
|
772
|
+
const fill = (text) => text.replace("{page}", title).replace("{app}", own(site.app.title));
|
|
773
|
+
const lines = [
|
|
774
|
+
`# ${fill(words.title)}`,
|
|
775
|
+
"",
|
|
776
|
+
fill(words.intro),
|
|
777
|
+
"",
|
|
778
|
+
`${words.parents} [${words.toPage}](../)`,
|
|
779
|
+
"",
|
|
780
|
+
...site.app.scope === void 0 ? [] : [words.scope, ""],
|
|
781
|
+
`## ${words.guideTitle}`,
|
|
782
|
+
"",
|
|
783
|
+
...words.guide.map((text) => `- ${text}`),
|
|
784
|
+
"",
|
|
785
|
+
`## ${words.startTitle}`,
|
|
786
|
+
"",
|
|
787
|
+
...words.start.map((text) => `- ${text}`),
|
|
788
|
+
""
|
|
789
|
+
];
|
|
790
|
+
if (site.app.tutor?.rules !== void 0) lines.push(`## ${words.appRules}`, "", inLanguage(site.app.tutor.rules, lang).trim(), "");
|
|
791
|
+
const first = exercises.find((exercise) => exercise.count > 0) ?? exercises[0];
|
|
792
|
+
const example = first === void 0 ? void 0 : first.count > 0 ? {
|
|
793
|
+
query: `${first.id}=12`,
|
|
794
|
+
link: `…/${page.chapter}/${page.id}/?${first.id}=12&via=tutor#${first.id}`
|
|
795
|
+
} : {
|
|
796
|
+
query: `uebung=${first.id}`,
|
|
797
|
+
link: `…/${page.chapter}/${page.id}/?via=tutor#${first.id}`
|
|
798
|
+
};
|
|
799
|
+
lines.push(`## ${words.whereTitle}`, "", ...words.where.flatMap((text) => !text.includes("{example}") && !text.includes("{link}") ? [`- ${text}`] : example === void 0 ? [] : [`- ${text.replace("{example}", example.query).replace("{link}", example.link)}`]), "", ...needsLines(lang), `## ${fill(words.contextTitle)}`, "");
|
|
800
|
+
if (page.description !== void 0) lines.push(own(page.description), "");
|
|
801
|
+
lines.push(...contentLines(site, page.blocks, lang, formulas, (heading, _anchor, depth) => `${"#".repeat(Math.min(3 + depth, 6))} ${heading}`));
|
|
802
|
+
for (const exercise of exercises) {
|
|
803
|
+
const part = exercisePart(site, exercise.block, lang, descriptions);
|
|
804
|
+
if (part === void 0) continue;
|
|
805
|
+
lines.push(`### ${words.exercise} ${part.id}`, "");
|
|
806
|
+
if (exercise.block.type === "generated") {
|
|
807
|
+
const format = part.lines.filter((text) => text.startsWith(`- ${tutorWords(lang).answerFormat}:`)).map(line);
|
|
808
|
+
lines.push(...generatedLines(exercise.block, site, lang, format));
|
|
809
|
+
} else {
|
|
810
|
+
lines.push(...part.lines.map((text) => {
|
|
811
|
+
const [, indent = "", rest = ""] = /^(\s*- )?(.*)$/.exec(text) ?? [];
|
|
812
|
+
return `${indent}${line(rest)}`;
|
|
813
|
+
}));
|
|
814
|
+
const declared = descriptions.get(exercise.block)?.exercise?.[lang];
|
|
815
|
+
if (exercise.count > 0) {
|
|
816
|
+
lines.push(`- ${words.linkNumber}: \`../?${exercise.id}=n&via=tutor#${exercise.id}\``);
|
|
817
|
+
const all = rows.get(exercise.id) ?? [];
|
|
818
|
+
const n = shown.get(exercise.id) ?? all.length;
|
|
819
|
+
if (declared !== void 0) lines.push(...componentTable(declared, lang));
|
|
820
|
+
else if (n > 0) lines.push(`- ${words.tasks}:`, ...all.slice(0, n));
|
|
821
|
+
if (n === 0 && all.length > 0) lines.push(`- ${words.none}`);
|
|
822
|
+
else if (n < all.length) lines.push(`- ${words.shown.replace("{shown}", String(n)).replace("{count}", String(all.length))}`);
|
|
823
|
+
} else lines.push(`- ${words.link}: [../?via=tutor#${exercise.id}](../?via=tutor#${exercise.id})`);
|
|
824
|
+
}
|
|
825
|
+
lines.push("");
|
|
826
|
+
}
|
|
827
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
828
|
+
}
|
|
829
|
+
const markdown = new Marked({
|
|
830
|
+
gfm: true,
|
|
831
|
+
extensions: [{
|
|
832
|
+
name: "formula",
|
|
833
|
+
level: "inline",
|
|
834
|
+
start: (source) => {
|
|
835
|
+
const at = source.indexOf("$");
|
|
836
|
+
return at < 0 ? void 0 : at;
|
|
837
|
+
},
|
|
838
|
+
tokenizer: (source) => {
|
|
839
|
+
const match = /^\$(?:\\.|[^\\$\n])+?\$/.exec(source);
|
|
840
|
+
return match === null ? void 0 : {
|
|
841
|
+
type: "formula",
|
|
842
|
+
raw: match[0]
|
|
843
|
+
};
|
|
844
|
+
},
|
|
845
|
+
renderer: (token) => escape(token.raw)
|
|
846
|
+
}]
|
|
847
|
+
});
|
|
848
|
+
/** The HTML document of a tutor page's Markdown `text`. */
|
|
849
|
+
function tutorPageDocument(text, site, page, lang) {
|
|
850
|
+
const words = wordsIn$1(lang);
|
|
851
|
+
const own = (value) => inLanguage(value, lang).replace(/\s+/g, " ").trim();
|
|
852
|
+
return tutorDocument(text, `${own(page.title)} – ${words.tutorPage} – ${own(site.app.title)}`, words.forTutor.replace("{page}", own(page.title)), lang);
|
|
853
|
+
}
|
|
854
|
+
/** The HTML document of a tutor page's Markdown `text`, with its `title` and `description`: lean, without script. */
|
|
855
|
+
function tutorDocument(text, title, description, lang) {
|
|
856
|
+
const body = markdown.parse(text, { async: false }).trim();
|
|
857
|
+
return [
|
|
858
|
+
"<!doctype html>",
|
|
859
|
+
`<html lang="${lang}">`,
|
|
860
|
+
"<head>",
|
|
861
|
+
"<meta charset=\"utf-8\" />",
|
|
862
|
+
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />",
|
|
863
|
+
`<title>${escape(title)}</title>`,
|
|
864
|
+
`<meta name="description" content="${escape(description)}" />`,
|
|
865
|
+
"<style>body{margin:0 auto;max-width:48rem;padding:1rem;font:1rem/1.5 system-ui,sans-serif;color:#1a1a1a;background:#fff;overflow-wrap:anywhere}pre{white-space:pre-wrap}code,pre{font-size:.95em}</style>",
|
|
866
|
+
"</head>",
|
|
867
|
+
"<body>",
|
|
868
|
+
"<main id=\"anleitung\">",
|
|
869
|
+
body,
|
|
870
|
+
"</main>",
|
|
871
|
+
"</body>",
|
|
872
|
+
"</html>",
|
|
873
|
+
""
|
|
874
|
+
].join("\n");
|
|
875
|
+
}
|
|
876
|
+
// runtime/explainer/assessment-page.ts
|
|
877
|
+
/** The address of a self-assessment's tutor page within its language tree. */
|
|
878
|
+
const assessmentTutorOf = (assessment) => `${assessment.local}${TUTOR_PAGE}/`;
|
|
879
|
+
const json$1 = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
880
|
+
/**
|
|
881
|
+
* The offer of a self-assessment on the start page or its chapter's page, in a <template> until the client module
|
|
882
|
+
* puts it into the page; it says "Dein letztes Ergebnis ansehen" when the device keeps one.
|
|
883
|
+
*/
|
|
884
|
+
function assessmentOffer(context, assessment) {
|
|
885
|
+
const { lang, site } = context;
|
|
886
|
+
const t = (key, params) => plain(message(lang, key, params));
|
|
887
|
+
const offer = assessment.chapter === void 0 ? "assessmentOfferApp" : "assessmentOfferChapter";
|
|
888
|
+
return [
|
|
889
|
+
`<template class="explainer-assessment-template">`,
|
|
890
|
+
`<section class="explainer-assessment-offer" aria-labelledby="selbsteinschaetzung" hidden data-key="${plain(assessment.key)}" data-storage="${plain(site.storage ?? "explainer")}">`,
|
|
891
|
+
`<h2 id="selbsteinschaetzung">${t("assessmentOfferTitle")}</h2>`,
|
|
892
|
+
`<p>${t(offer, { count: String(assessment.items.length) })}</p>`,
|
|
893
|
+
`<p><a class="explainer-assessment-link" href="${context.to(assessment.local)}" data-last="${t("assessmentOfferLast")}">${t("assessmentOfferStart")}</a></p>`,
|
|
894
|
+
`</section>`,
|
|
895
|
+
`</template>`
|
|
896
|
+
].join("\n");
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* One exercise of a self-assessment: its number, and its question, the place of its live task, or the component
|
|
900
|
+
* (`component`, its markup).
|
|
901
|
+
*/
|
|
902
|
+
function itemHtml(context, data, n, component = "") {
|
|
903
|
+
const { lang } = context;
|
|
904
|
+
const label = `<p class="explainer-assessment-label">${plain(message(lang, "assessmentTask", { n: String(n) }))}</p>`;
|
|
905
|
+
if (data.kind === "generated") return `${label}<div class="explainer-assessment-live"></div>`;
|
|
906
|
+
if (data.kind === "component") return `${label}${component}`;
|
|
907
|
+
if (data.kind === "open") return `${label}<div class="explainer-open-task-text">${data.task}</div><div class="explainer-open-live" hidden></div>`;
|
|
908
|
+
const { question, rendered } = data;
|
|
909
|
+
return `${label}${renderQuestion(question, {
|
|
910
|
+
lang,
|
|
911
|
+
format: (text) => rendered[text] ?? renderWithFormulas(text),
|
|
912
|
+
describe: (text) => formulaAlternative(text, lang),
|
|
913
|
+
...question.material === void 0 ? {} : { materialHtml: renderMaterial(question.material, context.material) }
|
|
914
|
+
})}`;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* The page of a self-assessment: the line for a reader without JavaScript, and, in a <template> until the client
|
|
918
|
+
* module puts it into the page, the intro with its start button, each of its pages with its exercises and the buttons to move on, the place of the
|
|
919
|
+
* result with the way to the tutor, and the client module's data.
|
|
920
|
+
*/
|
|
921
|
+
function assessmentMain(context, assessment) {
|
|
922
|
+
const { lang, site } = context;
|
|
923
|
+
const t = (key, params) => plain(message(lang, key, params));
|
|
924
|
+
const items = assessment.items.map((item) => {
|
|
925
|
+
if (item.block.type === "vue-component") {
|
|
926
|
+
const component = context.component(item.block, item.key);
|
|
927
|
+
return {
|
|
928
|
+
item,
|
|
929
|
+
data: {
|
|
930
|
+
kind: "component",
|
|
931
|
+
seeds: component?.seeds ?? [1]
|
|
932
|
+
},
|
|
933
|
+
html: component?.html ?? ""
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
const data = exerciseData(item.block, lang, site.data, context.resolve);
|
|
937
|
+
return {
|
|
938
|
+
item,
|
|
939
|
+
data: data.kind === "question" ? {
|
|
940
|
+
...data,
|
|
941
|
+
question: {
|
|
942
|
+
...data.question,
|
|
943
|
+
id: item.key
|
|
944
|
+
}
|
|
945
|
+
} : data
|
|
946
|
+
};
|
|
947
|
+
});
|
|
948
|
+
const steps = Array.from({ length: assessment.steps }, (_step, index) => {
|
|
949
|
+
const own = items.filter(({ item }) => item.step === index);
|
|
950
|
+
const last = index === assessment.steps - 1;
|
|
951
|
+
return [
|
|
952
|
+
`<section class="explainer-assessment-step" data-step="${index}" aria-labelledby="teil-${index + 1}" hidden>`,
|
|
953
|
+
`<h2 id="teil-${index + 1}" tabindex="-1">${t("assessmentStep", {
|
|
954
|
+
n: String(index + 1),
|
|
955
|
+
count: String(assessment.steps)
|
|
956
|
+
})}</h2>`,
|
|
957
|
+
`<ol class="explainer-assessment-items">`,
|
|
958
|
+
...own.map(({ item, data, html }) => `<li class="explainer-assessment-item" data-key="${item.key}">${itemHtml(context, data, items.findIndex((other) => other.item === item) + 1, html)}</li>`),
|
|
959
|
+
`</ol>`,
|
|
960
|
+
`<p class="explainer-assessment-nav">${index === 0 ? "" : `<button type="button" class="explainer-assessment-back">${t("assessmentBack")}</button> `}<button type="button" class="${last ? "explainer-assessment-finish" : "explainer-assessment-next"}">${t(last ? "assessmentFinish" : "assessmentNext")}</button></p>`,
|
|
961
|
+
`</section>`
|
|
962
|
+
].join("\n");
|
|
963
|
+
});
|
|
964
|
+
const tutor = assessmentTutorOf(assessment);
|
|
965
|
+
const all = assessments(site);
|
|
966
|
+
const whole = all.find((candidate) => candidate.chapter === void 0);
|
|
967
|
+
const data = {
|
|
968
|
+
key: assessment.key,
|
|
969
|
+
storage: site.storage ?? "explainer",
|
|
970
|
+
steps: assessment.steps,
|
|
971
|
+
items: items.map(({ item, data: exercise }) => ({
|
|
972
|
+
key: item.key,
|
|
973
|
+
ref: item.ref,
|
|
974
|
+
page: `${item.page.chapter}/${item.page.id}/`,
|
|
975
|
+
exercise: item.exercise,
|
|
976
|
+
step: item.step,
|
|
977
|
+
data: exercise
|
|
978
|
+
})),
|
|
979
|
+
pages: assessment.pages.map((page) => ({
|
|
980
|
+
page: `${page.chapter}/${page.id}/`,
|
|
981
|
+
title: titleOf(page, lang),
|
|
982
|
+
href: context.to(`${page.chapter}/${page.id}/`)
|
|
983
|
+
})),
|
|
984
|
+
chapters: site.chapters.map((chapter) => {
|
|
985
|
+
const own = all.find((candidate) => candidate.chapter === chapter) ?? (site.chapters.length === 1 ? whole : void 0);
|
|
986
|
+
return {
|
|
987
|
+
title: inLanguage(chapter.title, lang).replace(/\s+/g, " ").trim(),
|
|
988
|
+
...own === void 0 ? {} : { assessment: own.local },
|
|
989
|
+
pages: chapter.pages.map((page) => ({
|
|
990
|
+
title: titleOf(page, lang),
|
|
991
|
+
local: `${page.chapter}/${page.id}/`
|
|
992
|
+
}))
|
|
993
|
+
};
|
|
994
|
+
}),
|
|
995
|
+
whole: whole?.local ?? assessment.local,
|
|
996
|
+
root: context.to(""),
|
|
997
|
+
tutor,
|
|
998
|
+
markdown: `${tutor}${TUTOR_MARKDOWN}`,
|
|
999
|
+
ways: PROVIDERS.map((provider) => ({
|
|
1000
|
+
name: provider.name,
|
|
1001
|
+
url: provider.url,
|
|
1002
|
+
...provider.paste === true ? { paste: true } : {}
|
|
1003
|
+
})),
|
|
1004
|
+
placeholder: placeholderPrompt(lang)
|
|
1005
|
+
};
|
|
1006
|
+
const back = assessment.chapter === void 0 ? "" : `${assessment.chapter.id}/`;
|
|
1007
|
+
const unasked = assessment.pages.filter((page) => !assessment.items.some((item) => item.page === page));
|
|
1008
|
+
const unaskedText = unasked.length === 0 ? t("assessmentAllAsked") : t(unasked.length === 1 ? "assessmentLeftOutOne" : "assessmentLeftOut", { n: String(unasked.length) });
|
|
1009
|
+
return [
|
|
1010
|
+
`<h1>${t("assessmentTitle")}</h1>`,
|
|
1011
|
+
`<p class="explainer-assessment-nojs">${t("assessmentNojs")} <a href="${context.to(back)}">${plain(assessment.chapter === void 0 ? message(lang, "start") : inLanguage(assessment.chapter.title, lang))}</a></p>`,
|
|
1012
|
+
`<template class="explainer-assessment-template">`,
|
|
1013
|
+
`<div class="explainer-assessment" hidden>`,
|
|
1014
|
+
`<div class="explainer-assessment-intro">`,
|
|
1015
|
+
`<p>${t("assessmentIntro", { count: String(assessment.items.length) })} ${unaskedText}</p>`,
|
|
1016
|
+
...unasked.length === 0 ? [] : [
|
|
1017
|
+
`<ul class="explainer-assessment-unasked">`,
|
|
1018
|
+
...unasked.map((page) => `<li><a href="${context.to(`${page.chapter}/${page.id}/`)}">${plain(titleOf(page, lang))}</a></li>`),
|
|
1019
|
+
`</ul>`
|
|
1020
|
+
],
|
|
1021
|
+
`<p>${t("assessmentHow")}</p>`,
|
|
1022
|
+
`<p class="explainer-assessment-privacy">${t("assessmentPrivacy")}</p>`,
|
|
1023
|
+
`<p><button type="button" class="explainer-assessment-begin">${t("assessmentBegin")}</button></p>`,
|
|
1024
|
+
`</div>`,
|
|
1025
|
+
...steps,
|
|
1026
|
+
`<section class="explainer-assessment-result" aria-labelledby="ergebnis" hidden>`,
|
|
1027
|
+
`<h2 id="ergebnis" tabindex="-1">${t("assessmentResultTitle")}</h2>`,
|
|
1028
|
+
`<p class="explainer-assessment-score"></p>`,
|
|
1029
|
+
`<p class="explainer-assessment-advice"></p>`,
|
|
1030
|
+
`<ol class="explainer-assessment-pages"></ol>`,
|
|
1031
|
+
`<div class="explainer-assessment-tutor">`,
|
|
1032
|
+
`<h3>${t("assessmentTutorTitle")}</h3>`,
|
|
1033
|
+
`<p>${t("assessmentTutorIntro")}</p>`,
|
|
1034
|
+
`<p class="explainer-assessment-tutor-actions"><select class="explainer-assessment-way" aria-label="${t("tutorWay")}">${context.ways}</select> <button type="button" class="explainer-assessment-go"></button></p>`,
|
|
1035
|
+
`<p class="explainer-tutor-hint" role="status" aria-live="polite" hidden></p>`,
|
|
1036
|
+
`<p class="explainer-assessment-tutor-status" role="status" aria-live="polite"></p>`,
|
|
1037
|
+
`<p class="explainer-tutor-note">${t("tutorAccount")}</p>`,
|
|
1038
|
+
`<p><a class="explainer-assessment-tutor-page" href="${context.to(tutor)}">${t("assessmentTutorPage")}</a></p>`,
|
|
1039
|
+
`</div>`,
|
|
1040
|
+
`<p class="explainer-assessment-again"><button type="button" class="explainer-assessment-restart">${t("assessmentRestart")}</button> <button type="button" class="explainer-assessment-delete">${t("assessmentDelete")}</button></p>`,
|
|
1041
|
+
`<p class="explainer-assessment-status" role="status" aria-live="polite"></p>`,
|
|
1042
|
+
`</section>`,
|
|
1043
|
+
`<script type="application/json" class="explainer-assessment-data">${json$1(data)}<\/script>`,
|
|
1044
|
+
`</div>`,
|
|
1045
|
+
`</template>`
|
|
1046
|
+
].join("\n");
|
|
1047
|
+
}
|
|
1048
|
+
const WORDS$1 = {
|
|
1049
|
+
de: {
|
|
1050
|
+
title: "Selbsteinschätzung – für deinen KI-Tutor",
|
|
1051
|
+
description: "Was ein KI-Tutor zur Selbsteinschätzung von „{app}“ bekommt.",
|
|
1052
|
+
intro: "Du begleitest ein Kind beim Lernen mit der Lern-App „{app}“ von lernapps.net. Es hat die Selbsteinschätzung {scope} gemacht: Aufgaben aus den Seiten der App, ohne Rückmeldung zu einzelnen Aufgaben. Du bist sein Wegweiser: Du schickst es zu den Seiten, die noch nicht sitzen, und danach in eine neue Runde der Selbsteinschätzung ihres Kapitels. Erklärt und geübt wird auf den Seiten selbst; dort hat das Kind „Hilfe“, und jede Seite hat ihre eigene Tutor-Seite.",
|
|
1053
|
+
scopeApp: "der ganzen App",
|
|
1054
|
+
scopeChapter: "des Kapitels „{chapter}“",
|
|
1055
|
+
parents: "Eltern und Lehrkräfte sehen hier, was der Tutor bekommt.",
|
|
1056
|
+
toAssessment: "Zur Selbsteinschätzung",
|
|
1057
|
+
readTitle: "So liest du ein Ergebnis",
|
|
1058
|
+
read: [
|
|
1059
|
+
"Nach einer Runde fügt das Kind ein: das Ergebnis je Seite, eine Zeile je Aufgabe, diese Seite mit dem Ergebnis in der Adresse (z. B. `?a1=17-falsch-F1&a2=richtig&a3=offen&a4=teilweise-P2`) und den „Link für eine neue Runde“. Nimm das Ergebnis je Seite, wie es dasteht; rechne es nicht nach.",
|
|
1060
|
+
"Eine Seite mit allen Aufgaben richtig, und mindestens zwei, kann das Kind wahrscheinlich schon. Mit mindestens der Hälfte richtig soll es sie noch einmal ansehen, mit weniger genau lesen. Eine teilweise richtige Aufgabe zählt halb; `P2` nennt einen Punkt einer guten Antwort, der ihr fehlte. Eine nicht beantwortete Aufgabe (`offen`) zählt als nicht richtig. Eine Seite ohne Aufgabe wurde nicht abgefragt.",
|
|
1061
|
+
"Für jedes Kapitel gilt sein letztes Ergebnis: aus der letzten Runde dieses Kapitels, sonst aus der ersten Selbsteinschätzung. Merk es dir für die Kapitel, die noch kommen.",
|
|
1062
|
+
"„Ich habe auf … geübt:“ kommt von „Zurück zum Tutor“ auf einer Seite: deren Tutor-Seite mit den Nummern, dann je geübter Aufgabe, wie es lief, bei einer falschen mit Eingabe und Meldung der App."
|
|
1063
|
+
],
|
|
1064
|
+
guideTitle: "So leitest du an",
|
|
1065
|
+
guide: [
|
|
1066
|
+
"Antworte kurz: höchstens drei eigene Sätze und eine Frage, dazu, wo du das Kind hinschickst, der feste Satz und der Link. Keine Überschriften, keine Listen.",
|
|
1067
|
+
"Du bist Wegweiser, kein Lehrer: Erklär keinen Stoff, das tun die Seiten. Jede deiner Antworten schickt das Kind mit genau einem Link weiter. Coachen darfst du nur, wenn das Kind mit falschen Aufgaben von einer Seite zurückkommt (siehe unten).",
|
|
1068
|
+
"Geh Kapitel für Kapitel vor, in der Reihenfolge unten. Fang mit dem ersten Kapitel an, in dem eine Seite „Lies sie genau“ oder „Schau sie dir noch einmal an“ hat. Sag dem Kind zuerst in zwei Sätzen, was schon gut lief und womit ihr anfangt.",
|
|
1069
|
+
"Schick das Kind zu einer Seite nach der anderen, in der Reihenfolge der Seiten: zu denen zum genau Lesen und zum noch einmal Ansehen, und einmal zu einer Seite „nicht abgefragt“, wenn ihr bei ihr vorbeikommt. Sag dazu: „Lies die Seite und lös ihre Übungen. Oben und unten auf der Seite ist der Knopf ‚Zurück zum Tutor‘. Drück ihn und füg ein, was er kopiert.“",
|
|
1070
|
+
"Kommt das Kind mit „Ich habe auf … geübt:“ zurück und ist eine Aufgabe falsch oder offen, lade die genannte Tutor-Seite und hilf zu genau diesen Aufgaben, wie sie es sagt. Schick das Kind zum Lösen auf die Seite zurück, mit `?via=tutor`, und bitte es wieder um „Zurück zum Tutor“. Sind die genannten Aufgaben richtig, ist das Coachen vorbei: Es gilt wieder diese Anleitung, und ihr geht zur nächsten Seite.",
|
|
1071
|
+
"Sind die Seiten eines Kapitels durch, schick das Kind in eine neue Runde der Selbsteinschätzung des Kapitels: mit dem „Link für eine neue Runde“ aus seiner letzten Runde, sonst mit dem Link der Selbsteinschätzung des Kapitels aus den Links. Sag dazu: „Mach eine neue Runde. Am Ende drückst du ‚Zurück zum Tutor‘ und fügst das Ergebnis hier ein.“ Ein Kapitel ohne Selbsteinschätzung hat keine Runde.",
|
|
1072
|
+
"Nach jeder Runde entscheidest du: Kann das Kind alle abgefragten Seiten des Kapitels wahrscheinlich schon, geht ihr zum nächsten Kapitel. Sonst geht es noch einmal zu den Seiten, die nicht sitzen, und dann in eine neue Runde. Sitzt eine Seite nach zwei Runden ihres Kapitels noch nicht (die erste Selbsteinschätzung zählt nicht mit), sag dem Kind, es soll sie mit „Mit einem KI-Tutor lernen“ auf der Seite durchgehen, und geht danach weiter.",
|
|
1073
|
+
"Sind alle Kapitel durch, schick das Kind in eine Runde der ganzen App. Sitzt danach eine Seite nicht, geht ihr zu ihrem Kapitel wie oben. Sitzen alle abgefragten Seiten (eine Seite „nicht abgefragt“ zählt nicht), seid ihr fertig: Sag dem Kind in zwei Sätzen, was es geschafft hat, und dass es die Selbsteinschätzung später wiederholen kann, um zu sehen, ob es sitzt.",
|
|
1074
|
+
"Die Aufgaben der Selbsteinschätzung erklärst du nicht und verrätst keine Lösung: Ihre Seiten haben dieselben Übungen, dort übt das Kind.",
|
|
1075
|
+
"Eine Aufgabe „immer dieselbe Aufgabe“ (Liste unten) kann das Kind sich gemerkt haben. Sitzt eine Seite in einer neuen Runde nur mit solchen Aufgaben, frag kurz, warum die Antwort stimmt, bevor ihr weitergeht.",
|
|
1076
|
+
"Jeden Link nimmst du genau so aus dem Block „Links für meinen Tutor“ in der Nachricht des Kindes. Bilde keine Adresse selbst, und schick dem Kind nie eine Tutor-Seite (eine Adresse mit `tutor/`): Die ist nur für dich."
|
|
1077
|
+
],
|
|
1078
|
+
appRules: "Regeln dieser App",
|
|
1079
|
+
chaptersTitle: "Die Kapitel und ihre Seiten, in ihrer Reihenfolge",
|
|
1080
|
+
chapter: "Kapitel",
|
|
1081
|
+
assessmentOf: "Selbsteinschätzung des Kapitels",
|
|
1082
|
+
noAssessment: "keine Selbsteinschätzung",
|
|
1083
|
+
whole: "Selbsteinschätzung der ganzen App",
|
|
1084
|
+
page: "Seite",
|
|
1085
|
+
tutorPage: "Tutor-Seite",
|
|
1086
|
+
asked: "Aufgaben",
|
|
1087
|
+
unasked: "nicht abgefragt",
|
|
1088
|
+
itemsTitle: "Die Aufgaben dieser Selbsteinschätzung",
|
|
1089
|
+
item: "{key}: Übung {exercise} der Seite „{page}“",
|
|
1090
|
+
same: " (immer dieselbe Aufgabe, auch auf der Seite: richtig kann gemerkt sein)",
|
|
1091
|
+
quoted: "„{title}“"
|
|
1092
|
+
},
|
|
1093
|
+
en: {
|
|
1094
|
+
title: "Self-assessment – for your AI tutor",
|
|
1095
|
+
description: "What an AI tutor gets for the self-assessment of “{app}”.",
|
|
1096
|
+
intro: "You accompany a child learning with the learning app “{app}” of lernapps.net. It did the self-assessment {scope}: tasks from the pages of the app, without feedback on any single task. You are its guide: you send it to the pages that do not stick yet, and then into a new round of the self-assessment of their chapter. Explaining and practising happen on the pages themselves; there the child has “Help”, and every page has a tutor page of its own.",
|
|
1097
|
+
scopeApp: "of the whole app",
|
|
1098
|
+
scopeChapter: "of the chapter “{chapter}”",
|
|
1099
|
+
parents: "Parents and teachers see here what the tutor gets.",
|
|
1100
|
+
toAssessment: "To the self-assessment",
|
|
1101
|
+
readTitle: "How you read a result",
|
|
1102
|
+
read: [
|
|
1103
|
+
"After a round the child pastes: the result per page, a line per task, this page with the result in its address (e.g. `?a1=17-falsch-F1&a2=richtig&a3=offen&a4=teilweise-P2`) and the “Link for a new round”. Take the result per page as it stands; do not work it out again.",
|
|
1104
|
+
"A page with all its tasks right, and two at least, the child probably knows already. With at least half of them right it should look at it again, with fewer read it closely. A partly right task (`teilweise`) counts half; `P2` names a point of a good answer it lacked. A task not answered (`offen`) counts as not right. A page without a task was not asked.",
|
|
1105
|
+
"For each chapter its last result holds: of the last round of this chapter, else of the first self-assessment. Keep it in mind for the chapters still to come.",
|
|
1106
|
+
"“I practised on …:” comes from “Back to the tutor” on a page: its tutor page with the numbers, then for each task practised how it went, a wrong one with the input and the app's message."
|
|
1107
|
+
],
|
|
1108
|
+
guideTitle: "How you guide",
|
|
1109
|
+
guide: [
|
|
1110
|
+
"Answer briefly: three sentences of your own and one question at most, plus, where you send the child, the fixed sentence and the link. No headings, no lists.",
|
|
1111
|
+
"You are the guide, not a teacher: explain no subject matter, the pages do. Each of your answers sends the child on with exactly one link. You may coach only when the child comes back from a page with wrong tasks (see below).",
|
|
1112
|
+
"Go chapter by chapter, in the order below. Start with the first chapter that has a page “Read it closely” or “Look at it again”. First tell the child in two sentences what went well already and where you start.",
|
|
1113
|
+
"Send the child to one page after another, in the order of the pages: to those to read closely and to look at again, and once to a page “not asked” when you come by it. Add: “Read the page and do its exercises. At the top and the bottom of the page there is the button ‘Back to the tutor’. Press it and paste what it copies.”",
|
|
1114
|
+
"If the child comes back with “I practised on …:” and a task is wrong or open, load the tutor page it names and help with exactly these tasks, as it says. Send the child back to the page to solve them, with `?via=tutor`, and ask it for “Back to the tutor” again. Once the tasks named are right, the coaching is over: this guide applies again, and you go on to the next page.",
|
|
1115
|
+
"Once the pages of a chapter are done, send the child into a new round of the chapter's self-assessment: with the “Link for a new round” of its last round, else with the link of the chapter's self-assessment from the links. Add: “Do a new round. At the end press ‘Back to the tutor’ and paste the result here.” A chapter without a self-assessment has no round.",
|
|
1116
|
+
"After each round you decide: if the child probably knows every page of the chapter asked, you go on to the next chapter. Else it goes once more to the pages that do not stick, then into a new round. If a page still does not stick after two rounds of its chapter (the first self-assessment does not count), tell the child to go through it with “Learn with an AI tutor” on the page, and go on after that.",
|
|
1117
|
+
"Once every chapter is done, send the child into a round of the whole app. If a page does not stick then, go to its chapter as above. If every page asked sticks (a page “not asked” does not count), you are done: tell the child in two sentences what it has achieved, and that it can repeat the self-assessment later to see whether it sticks.",
|
|
1118
|
+
"You do not explain the tasks of the self-assessment and give no solution away: their pages have the same exercises, the child practises there.",
|
|
1119
|
+
"A task “always the same task” (list below) the child may have remembered. If a page sticks in a new round only by such tasks, ask briefly why the answer is right before you go on.",
|
|
1120
|
+
"Take every link exactly as it is from the block “Links for my tutor” in the child's message. Build no address yourself, and never send the child a tutor page (an address with `tutor/`): it is for you only."
|
|
1121
|
+
],
|
|
1122
|
+
appRules: "Rules of this app",
|
|
1123
|
+
chaptersTitle: "The chapters and their pages, in their order",
|
|
1124
|
+
chapter: "Chapter",
|
|
1125
|
+
assessmentOf: "self-assessment of the chapter",
|
|
1126
|
+
noAssessment: "no self-assessment",
|
|
1127
|
+
whole: "Self-assessment of the whole app",
|
|
1128
|
+
page: "Page",
|
|
1129
|
+
tutorPage: "Tutor page",
|
|
1130
|
+
asked: "Tasks",
|
|
1131
|
+
unasked: "not asked",
|
|
1132
|
+
itemsTitle: "The tasks of this self-assessment",
|
|
1133
|
+
item: "{key}: exercise {exercise} of the page “{page}”",
|
|
1134
|
+
same: " (always the same task, on its page too: right may be remembered)",
|
|
1135
|
+
quoted: "“{title}”"
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
const wordsIn = (lang) => WORDS$1[lang] ?? WORDS$1["de"];
|
|
1139
|
+
/**
|
|
1140
|
+
* The tutor page of a self-assessment in `lang`, as Markdown, its links relative to its folder <assessment>/tutor/: the
|
|
1141
|
+
* guide of a tutor who leads the learner chapter by chapter to the pages that do not stick and into a new round of
|
|
1142
|
+
* the chapter's self-assessment, how to read a result, every chapter of the app with its pages (the exercises of this
|
|
1143
|
+
* self-assessment by key, or not asked) and the link to its self-assessment, and the exercise behind each key.
|
|
1144
|
+
*/
|
|
1145
|
+
function assessmentTutorMarkdown(site, assessment, lang) {
|
|
1146
|
+
const words = wordsIn(lang);
|
|
1147
|
+
const own = (text) => inLanguage(text, lang).replace(/\s+/g, " ").trim();
|
|
1148
|
+
const app = own(site.app.title);
|
|
1149
|
+
const scope = assessment.chapter === void 0 ? words.scopeApp : words.scopeChapter.replace("{chapter}", own(assessment.chapter.title));
|
|
1150
|
+
const here = assessmentTutorOf(assessment);
|
|
1151
|
+
const all = assessments(site);
|
|
1152
|
+
const whole = all.find((candidate) => candidate.chapter === void 0);
|
|
1153
|
+
const lines = [
|
|
1154
|
+
`# ${words.title}`,
|
|
1155
|
+
"",
|
|
1156
|
+
words.intro.replace("{app}", app).replace("{scope}", scope),
|
|
1157
|
+
"",
|
|
1158
|
+
`${words.parents} [${words.toAssessment}](../)`,
|
|
1159
|
+
"",
|
|
1160
|
+
`## ${words.readTitle}`,
|
|
1161
|
+
"",
|
|
1162
|
+
...words.read.map((text) => `- ${text}`),
|
|
1163
|
+
"",
|
|
1164
|
+
`## ${words.guideTitle}`,
|
|
1165
|
+
"",
|
|
1166
|
+
...words.guide.map((text) => `- ${text.replace("{here}", here)}`),
|
|
1167
|
+
""
|
|
1168
|
+
];
|
|
1169
|
+
if (site.app.tutor?.rules !== void 0) lines.push(`## ${words.appRules}`, "", inLanguage(site.app.tutor.rules, lang).trim(), "");
|
|
1170
|
+
lines.push(...needsLines(lang), `## ${words.chaptersTitle}`, "");
|
|
1171
|
+
for (const chapter of site.chapters) {
|
|
1172
|
+
const its = all.find((candidate) => candidate.chapter === chapter) ?? (site.chapters.length === 1 ? whole : void 0);
|
|
1173
|
+
lines.push(`### ${words.chapter} ${words.quoted.replace("{title}", own(chapter.title))}`, "", `- ${its === void 0 ? words.noAssessment : words.assessmentOf}`);
|
|
1174
|
+
for (const page of chapter.pages) {
|
|
1175
|
+
const keys = assessment.items.filter((item) => item.page === page).map((item) => item.key.toUpperCase());
|
|
1176
|
+
const asked = assessment.pages.includes(page) ? ` · ${keys.length === 0 ? words.unasked : `${words.asked} ${keys.join(", ")}`}` : "";
|
|
1177
|
+
lines.push(`- ${words.quoted.replace("{title}", titleOf(page, lang))}${asked}`);
|
|
1178
|
+
}
|
|
1179
|
+
lines.push("");
|
|
1180
|
+
}
|
|
1181
|
+
lines.push(`## ${words.itemsTitle}`, "");
|
|
1182
|
+
for (const item of assessment.items) lines.push(`- ${words.item.replace("{key}", item.key.toUpperCase()).replace("{exercise}", item.exercise).replace("{page}", titleOf(item.page, lang))}${drawsTasks(item.block) ? "" : words.same}`);
|
|
1183
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
1184
|
+
}
|
|
1185
|
+
/** The tutor page of a self-assessment in `lang` as a whole HTML document. */
|
|
1186
|
+
function assessmentTutorDocument(markdown, site, lang) {
|
|
1187
|
+
const words = wordsIn(lang);
|
|
1188
|
+
const app = inLanguage(site.app.title, lang).replace(/\s+/g, " ").trim();
|
|
1189
|
+
return tutorDocument(markdown, `${words.title} – ${app}`, words.description.replace("{app}", app), lang);
|
|
1190
|
+
}
|
|
1191
|
+
// runtime/explainer/render.ts
|
|
1192
|
+
function sizeless(site, image) {
|
|
1193
|
+
const file = site.images.get(image);
|
|
1194
|
+
return file !== void 0 && isSizeless(file);
|
|
1195
|
+
}
|
|
1196
|
+
/** What rendering one page needs. */
|
|
1197
|
+
var Context = class {
|
|
1198
|
+
locale;
|
|
1199
|
+
site;
|
|
1200
|
+
route;
|
|
1201
|
+
islands;
|
|
1202
|
+
descriptions;
|
|
1203
|
+
/** What the blocks that show content need from this page. */
|
|
1204
|
+
material;
|
|
1205
|
+
/** Whether a component was rendered. */
|
|
1206
|
+
hasIslands = false;
|
|
1207
|
+
constructor(site, route, islands, descriptions) {
|
|
1208
|
+
this.site = site;
|
|
1209
|
+
this.route = route;
|
|
1210
|
+
this.islands = islands;
|
|
1211
|
+
this.descriptions = descriptions;
|
|
1212
|
+
this.locale = localeOf(route.lang);
|
|
1213
|
+
this.material = {
|
|
1214
|
+
lang: route.lang,
|
|
1215
|
+
href: (image) => relative(route.path, image),
|
|
1216
|
+
resolve: this.resolve,
|
|
1217
|
+
sizeless: (image) => sizeless(site, image)
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
get lang() {
|
|
1221
|
+
return this.route.lang;
|
|
1222
|
+
}
|
|
1223
|
+
t(key, params) {
|
|
1224
|
+
return message(this.lang, key, params);
|
|
1225
|
+
}
|
|
1226
|
+
/** The text in this page's language, as plain HTML. */
|
|
1227
|
+
text(text) {
|
|
1228
|
+
return plain(inLanguage(text, this.lang));
|
|
1229
|
+
}
|
|
1230
|
+
/** A link from this page to `local` (an address within the language tree, e.g. "prozent/prozentwert/#regel"). */
|
|
1231
|
+
to(local) {
|
|
1232
|
+
return relative(this.route.path, languageRoot(this.lang) + local);
|
|
1233
|
+
}
|
|
1234
|
+
/** Where a link inside a text leads: /<chapter>/<page>/#<section> within this language's tree. */
|
|
1235
|
+
resolve = (href) => this.to(href.replace(/^\//, ""));
|
|
1236
|
+
markdown(text) {
|
|
1237
|
+
return renderMarkdown(inLanguage(text, this.lang), this.resolve);
|
|
1238
|
+
}
|
|
1239
|
+
inline(text) {
|
|
1240
|
+
return renderInline(inLanguage(text, this.lang), this.resolve);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
const external = (url, text = url) => `<a href="${plain(url)}" rel="noopener">${plain(text)}</a>`;
|
|
1244
|
+
function dateLabel(context, event) {
|
|
1245
|
+
const date = String(event.date);
|
|
1246
|
+
const match = /^(-?\d{1,4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(date);
|
|
1247
|
+
const year = Number(match?.[1] ?? 0);
|
|
1248
|
+
const datetime = year >= 0 && match !== null ? date.replace(/^\d+/, (digits) => digits.padStart(4, "0")) : void 0;
|
|
1249
|
+
if (event.label !== void 0) return {
|
|
1250
|
+
label: context.text(event.label),
|
|
1251
|
+
...datetime === void 0 ? {} : { datetime }
|
|
1252
|
+
};
|
|
1253
|
+
if (year < 0) return { label: plain(context.t("beforeChrist", { year: String(-year) })) };
|
|
1254
|
+
let label = String(year);
|
|
1255
|
+
if (match?.[2] !== void 0) {
|
|
1256
|
+
const when = new Date(Date.UTC(year, Number(match[2]) - 1, Number(match[3] ?? 1)));
|
|
1257
|
+
when.setUTCFullYear(year);
|
|
1258
|
+
label = new Intl.DateTimeFormat(context.locale, {
|
|
1259
|
+
timeZone: "UTC",
|
|
1260
|
+
year: "numeric",
|
|
1261
|
+
month: "long",
|
|
1262
|
+
...match[3] === void 0 ? {} : { day: "numeric" }
|
|
1263
|
+
}).format(when);
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
label: plain(label),
|
|
1267
|
+
...datetime === void 0 ? {} : { datetime }
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function renderEvents(context, events) {
|
|
1271
|
+
return `<ol class="explainer-timeline">${events.map((event) => {
|
|
1272
|
+
const { label, datetime } = dateLabel(context, event);
|
|
1273
|
+
const when = datetime === void 0 ? `<span class="explainer-date">${label}</span>` : `<time class="explainer-date" datetime="${datetime}">${label}</time>`;
|
|
1274
|
+
const text = event.text === void 0 ? "" : `<div class="explainer-event-text">${context.markdown(event.text)}</div>`;
|
|
1275
|
+
return `<li><p class="explainer-event">${when} <strong>${context.text(event.title)}</strong></p>${text}</li>`;
|
|
1276
|
+
}).join("")}</ol>`;
|
|
1277
|
+
}
|
|
1278
|
+
/** The blocks, at heading level `level` (the level of the heading they are under: 1 for the page). */
|
|
1279
|
+
function renderBlocks(context, blocks, level, anchor = []) {
|
|
1280
|
+
return blocks.map((block) => renderBlock(context, block, level, anchor)).join("\n");
|
|
1281
|
+
}
|
|
1282
|
+
function renderBlock(context, block, level, anchor) {
|
|
1283
|
+
switch (block.type) {
|
|
1284
|
+
case "section": {
|
|
1285
|
+
const id = [...anchor, block.id];
|
|
1286
|
+
return [
|
|
1287
|
+
`<section class="explainer-section">`,
|
|
1288
|
+
`<h${level + 1} id="${id.join("/")}">${context.text(block.title)}</h${level + 1}>`,
|
|
1289
|
+
renderBlocks(context, block.blocks, level + 1, id),
|
|
1290
|
+
`</section>`
|
|
1291
|
+
].join("\n");
|
|
1292
|
+
}
|
|
1293
|
+
case "example":
|
|
1294
|
+
case "note": return [
|
|
1295
|
+
`<div class="explainer-box explainer-${block.type}">`,
|
|
1296
|
+
`<h${level + 1} class="explainer-box-title">${context.text(block.title)}</h${level + 1}>`,
|
|
1297
|
+
renderBlocks(context, block.blocks, level + 1, anchor),
|
|
1298
|
+
`</div>`
|
|
1299
|
+
].join("\n");
|
|
1300
|
+
case "text":
|
|
1301
|
+
case "formula":
|
|
1302
|
+
case "figure":
|
|
1303
|
+
case "labelled-figure":
|
|
1304
|
+
case "table": return renderMaterial([block], context.material);
|
|
1305
|
+
case "timeline": return renderEvents(context, timelineEvents(context.site, block));
|
|
1306
|
+
case "definition": {
|
|
1307
|
+
const id = termId(block.term);
|
|
1308
|
+
return [
|
|
1309
|
+
`<dl class="explainer-definition">`,
|
|
1310
|
+
`<dt><dfn id="begriff-${id}"><a href="${context.to(`glossar/#${id}`)}">${context.text(block.term)}</a></dfn></dt>`,
|
|
1311
|
+
`<dd>${context.markdown(block.explanation)}</dd>`,
|
|
1312
|
+
`</dl>`
|
|
1313
|
+
].join("");
|
|
1314
|
+
}
|
|
1315
|
+
case "quote":
|
|
1316
|
+
case "code": return renderMaterial([block], context.material);
|
|
1317
|
+
case "video": return renderVideo(block, context.lang, context.site.storage ?? "explainer");
|
|
1318
|
+
case "vue-component": return renderComponent(context, block);
|
|
1319
|
+
default: return renderExercise(block, context.lang, context.site.data, context.resolve, context.material, context.site.storage ?? "explainer");
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
const json = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1323
|
+
/**
|
|
1324
|
+
* A vue-component block: its island, hidden until it hydrates, its nojs text, its props and its feedback. An exercise
|
|
1325
|
+
* component has its anchor, its task number with "Neue Aufgabe" (hidden until it hydrates), its island in a form with
|
|
1326
|
+
* the explainer's "Prüfen" (the row of the shared questions, hidden until it hydrates; Enter in a field of the
|
|
1327
|
+
* component checks too), and as its no-JS text the task of number 1 as its describe states it, with the solution
|
|
1328
|
+
* folded, under the block's own nojs text if it has one.
|
|
1329
|
+
*/
|
|
1330
|
+
function renderComponent(context, block) {
|
|
1331
|
+
const use = context.site.components.find((candidate) => candidate.block === block);
|
|
1332
|
+
if (use === void 0) return "";
|
|
1333
|
+
context.hasIslands = true;
|
|
1334
|
+
const { component } = use;
|
|
1335
|
+
const markup = context.islands.get(block)?.get(context.lang) ?? "";
|
|
1336
|
+
const messages = questionMessagesIn(context.lang);
|
|
1337
|
+
const feedback = component.exercise ? `<p class="question-feedback" data-right="${plain(messages.right)}" data-wrong="${plain(messages.wrong)}"></p>` : "";
|
|
1338
|
+
const first = context.descriptions.get(block)?.tasks[context.lang]?.[0];
|
|
1339
|
+
const number = (seed) => plain(context.t("taskNumber", { n: String(seed) }));
|
|
1340
|
+
const described = first === void 0 ? "" : [
|
|
1341
|
+
`<div class="explainer-described">`,
|
|
1342
|
+
`<p class="explainer-task-number">${number(first.number)}</p>`,
|
|
1343
|
+
`<div class="explainer-task">${renderMarkdown(first.task, context.resolve)}</div>`,
|
|
1344
|
+
`<details class="explainer-solution"><summary>${plain(messages.solution)}</summary>${renderMarkdown(first.solution, context.resolve)}</details>`,
|
|
1345
|
+
`</div>`
|
|
1346
|
+
].join("");
|
|
1347
|
+
const nojs = (block.nojs === void 0 ? "" : context.markdown(block.nojs)) + described;
|
|
1348
|
+
const controls = component.exercise ? `<p class="explainer-task-controls" hidden><span class="explainer-task-number" tabindex="-1" data-template="${plain(context.t("taskNumber"))}">${number(1)}</span> <button type="button" class="explainer-new-task" aria-label="${plain(context.t("newTask"))}" title="${plain(context.t("newTask"))}">${NEW_TASK_ICON}</button></p>` : "";
|
|
1349
|
+
return [
|
|
1350
|
+
`<div class="explainer-component" ${[
|
|
1351
|
+
...use.id === void 0 ? [] : [`id="${use.id}"`],
|
|
1352
|
+
`data-component="${component.name}"`,
|
|
1353
|
+
...component.exercise ? [`data-exercise data-seeds="${JSON.stringify(context.descriptions.get(block)?.seeds ?? [1])}"`] : [],
|
|
1354
|
+
...component.checked ? ["data-checked"] : [],
|
|
1355
|
+
...component.caption ? [`data-nojs="caption" data-caption="${plain(context.t("textDescription"))}"`] : []
|
|
1356
|
+
].join(" ")}>`,
|
|
1357
|
+
controls,
|
|
1358
|
+
component.exercise ? `<form class="explainer-component-form" novalidate><div class="explainer-island" hidden>${markup}</div><p class="question-actions" hidden><button type="submit" class="question-check" disabled>${plain(messages.check)}</button></p></form>` : `<div class="explainer-island" hidden>${markup}</div>`,
|
|
1359
|
+
`<div class="explainer-nojs">${nojs}</div>`,
|
|
1360
|
+
feedback,
|
|
1361
|
+
`<script type="application/json" class="explainer-component-props">${json(propsIn(block.props, component, context.lang))}<\/script>`,
|
|
1362
|
+
`</div>`
|
|
1363
|
+
].filter((part) => part !== "").join("\n");
|
|
1364
|
+
}
|
|
1365
|
+
/** The contents of a page with three or more sections: the sections and their subsections. */
|
|
1366
|
+
function pageContents(context, blocks) {
|
|
1367
|
+
if (blocks.filter((block) => block.type === "section").length < 3) return "";
|
|
1368
|
+
const list = (items, anchor) => `<ol>${items.filter((block) => block.type === "section").map((section) => {
|
|
1369
|
+
const id = [...anchor, section.id];
|
|
1370
|
+
const inner = section.blocks.some((block) => block.type === "section") ? list(section.blocks, id) : "";
|
|
1371
|
+
return `<li><a href="#${id.join("/")}">${context.text(section.title)}</a>${inner}</li>`;
|
|
1372
|
+
}).join("")}</ol>`;
|
|
1373
|
+
return `<nav class="explainer-toc" aria-label="${plain(context.t("onThisPage"))}"><p class="explainer-toc-title">${plain(context.t("onThisPage"))}</p>${list(blocks, [])}</nav>`;
|
|
1374
|
+
}
|
|
1375
|
+
function header(context) {
|
|
1376
|
+
const { site, route } = context;
|
|
1377
|
+
return pageHeader({
|
|
1378
|
+
...route,
|
|
1379
|
+
languages: site.app.languages,
|
|
1380
|
+
label: plain(context.t("languages")),
|
|
1381
|
+
name: (lang) => plain(message(lang, "language"))
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
function breadcrumbs(context) {
|
|
1385
|
+
const { route, site } = context;
|
|
1386
|
+
if (route.kind === "start") return "";
|
|
1387
|
+
const items = [`<li><a href="${context.to("")}">${plain(context.t("start"))}</a></li>`];
|
|
1388
|
+
if ((route.kind === "page" || route.kind === "assessment") && route.chapter !== void 0 && !hiddenChapter(site)) items.push(`<li><a href="${context.to(`${route.chapter.id}/`)}">${context.text(route.chapter.title)}</a></li>`);
|
|
1389
|
+
items.push(`<li aria-current="page">${plain(route.title)}</li>`);
|
|
1390
|
+
return `<nav class="explainer-breadcrumbs" aria-label="${plain(context.t("breadcrumbs"))}"><ol>${items.join("")}</ol></nav>`;
|
|
1391
|
+
}
|
|
1392
|
+
function pager(context) {
|
|
1393
|
+
const { route, site } = context;
|
|
1394
|
+
if (route.page === void 0) return "";
|
|
1395
|
+
const pages = contentPages(site);
|
|
1396
|
+
const index = pages.indexOf(route.page);
|
|
1397
|
+
const link = (rel, key, offset) => {
|
|
1398
|
+
const other = pages[index + offset];
|
|
1399
|
+
if (other === void 0) return "";
|
|
1400
|
+
const title = inLanguage(other.title, context.lang);
|
|
1401
|
+
return `<a rel="${rel}" href="${context.to(`${other.chapter}/${other.id}/`)}">${plain(context.t(key, { title }))}</a>`;
|
|
1402
|
+
};
|
|
1403
|
+
return `<nav class="explainer-pager" aria-label="${plain(context.t("pager"))}">${link("prev", "previous", -1)}${link("next", "next", 1)}</nav>`;
|
|
1404
|
+
}
|
|
1405
|
+
function pageList(context, pages) {
|
|
1406
|
+
return `<ol class="explainer-pages">${pages.map((page) => {
|
|
1407
|
+
const description = page.description === void 0 ? "" : ` <span class="explainer-description">${context.text(page.description)}</span>`;
|
|
1408
|
+
return `<li><a href="${context.to(`${page.chapter}/${page.id}/`)}">${context.text(page.title)}</a>${description}</li>`;
|
|
1409
|
+
}).join("")}</ol>`;
|
|
1410
|
+
}
|
|
1411
|
+
function main(context) {
|
|
1412
|
+
const { route, site } = context;
|
|
1413
|
+
const title = `<h1>${plain(route.title)}</h1>`;
|
|
1414
|
+
switch (route.kind) {
|
|
1415
|
+
case "start": {
|
|
1416
|
+
const contents = hiddenChapter(site) ? pageList(context, site.chapters[0]?.pages ?? []) : `<ol class="explainer-chapters">${site.chapters.map((chapter) => {
|
|
1417
|
+
const summary = chapter.summary === void 0 ? "" : context.markdown(chapter.summary);
|
|
1418
|
+
return `<li><a class="explainer-chapter-link" href="${context.to(`${chapter.id}/`)}">${context.text(chapter.title)}</a>${summary}${pageList(context, chapter.pages)}</li>`;
|
|
1419
|
+
}).join("")}</ol>`;
|
|
1420
|
+
const glossary = terms(site).length === 0 ? "" : `<p><a href="${context.to("glossar/")}">${plain(context.t("glossary"))}</a></p>`;
|
|
1421
|
+
const sources = site.app.sources === void 0 ? "" : `<section class="explainer-sources"><h2>${plain(context.t("sources"))}</h2><p>${plain(context.t("sourcesIntro"))}</p><ul>${site.app.sources.map((source) => {
|
|
1422
|
+
const text = inLanguage(source.title, context.lang);
|
|
1423
|
+
return `<li>${source.url === void 0 ? plain(text) : external(source.url, text)}</li>`;
|
|
1424
|
+
}).join("")}</ul></section>`;
|
|
1425
|
+
const assessment = assessmentOf(site);
|
|
1426
|
+
return [
|
|
1427
|
+
title,
|
|
1428
|
+
`<div class="explainer-intro">${context.markdown(site.app.intro)}</div>`,
|
|
1429
|
+
assessment === void 0 ? "" : assessmentOffer(assessmentContext(context), assessment),
|
|
1430
|
+
`<nav class="explainer-contents" aria-labelledby="inhalt"><h2 id="inhalt">${plain(context.t("contents"))}</h2>${contents}</nav>`,
|
|
1431
|
+
glossary,
|
|
1432
|
+
tutorAbout(context),
|
|
1433
|
+
sources
|
|
1434
|
+
].filter((part) => part !== "").join("\n");
|
|
1435
|
+
}
|
|
1436
|
+
case "chapter": {
|
|
1437
|
+
const chapter = route.chapter;
|
|
1438
|
+
if (chapter === void 0) return title;
|
|
1439
|
+
const summary = chapter.summary === void 0 ? "" : `<div class="explainer-intro">${context.markdown(chapter.summary)}</div>`;
|
|
1440
|
+
const assessment = assessmentOf(site, chapter.id);
|
|
1441
|
+
return [
|
|
1442
|
+
title,
|
|
1443
|
+
summary,
|
|
1444
|
+
assessment === void 0 ? "" : assessmentOffer(assessmentContext(context), assessment),
|
|
1445
|
+
`<nav class="explainer-contents" aria-label="${plain(context.t("pages"))}">${pageList(context, chapter.pages)}</nav>`
|
|
1446
|
+
].filter((part) => part !== "").join("\n");
|
|
1447
|
+
}
|
|
1448
|
+
case "assessment": return route.assessment === void 0 ? title : assessmentMain(assessmentContext(context), route.assessment);
|
|
1449
|
+
case "glossary": {
|
|
1450
|
+
const collator = new Intl.Collator(context.locale);
|
|
1451
|
+
const entries = terms(site).sort((a, b) => collator.compare(inLanguage(a.term, context.lang), inLanguage(b.term, context.lang))).map((term) => {
|
|
1452
|
+
const page = inLanguage(term.page.title, context.lang);
|
|
1453
|
+
const back = `<a href="${context.to(`${term.page.chapter}/${term.page.id}/#begriff-${term.id}`)}">${plain(context.t("explainedOn", { page }))}</a>`;
|
|
1454
|
+
return `<dt id="${term.id}"><dfn>${context.text(term.term)}</dfn></dt><dd>${context.markdown(term.explanation)}<p>${back}</p></dd>`;
|
|
1455
|
+
});
|
|
1456
|
+
return [
|
|
1457
|
+
title,
|
|
1458
|
+
`<p>${plain(context.t("glossaryIntro"))}</p>`,
|
|
1459
|
+
`<dl class="explainer-glossary">${entries.join("")}</dl>`
|
|
1460
|
+
].join("\n");
|
|
1461
|
+
}
|
|
1462
|
+
case "page": {
|
|
1463
|
+
const page = route.page;
|
|
1464
|
+
if (page === void 0) return title;
|
|
1465
|
+
return [
|
|
1466
|
+
`<div class="explainer-title-row">`,
|
|
1467
|
+
title,
|
|
1468
|
+
tutorSection(context),
|
|
1469
|
+
`</div>`,
|
|
1470
|
+
pageContents(context, page.blocks),
|
|
1471
|
+
renderBlocks(context, page.blocks, 1)
|
|
1472
|
+
].join("\n");
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/** What a part of a self-assessment needs of the page it is on (assessment-page.ts). */
|
|
1477
|
+
const assessmentContext = (context) => ({
|
|
1478
|
+
site: context.site,
|
|
1479
|
+
lang: context.lang,
|
|
1480
|
+
to: (local) => context.to(local),
|
|
1481
|
+
resolve: context.resolve,
|
|
1482
|
+
material: context.material,
|
|
1483
|
+
ways: wayOptions(context),
|
|
1484
|
+
component: (block, key) => assessmentComponent(context, block, key)
|
|
1485
|
+
});
|
|
1486
|
+
/**
|
|
1487
|
+
* An exercise component as one exercise of a self-assessment, by its key: its island with the task of number 1 as the
|
|
1488
|
+
* build rendered it, its props and its task numbers, without "Prüfen", feedback, task number or nojs text (the
|
|
1489
|
+
* self-assessment runs with JavaScript only); the islands hydrate it with the task the self-assessment draws.
|
|
1490
|
+
*/
|
|
1491
|
+
function assessmentComponent(context, block, key) {
|
|
1492
|
+
const use = context.site.components.find((candidate) => candidate.block === block);
|
|
1493
|
+
if (use === void 0 || !use.component.exercise) return void 0;
|
|
1494
|
+
context.hasIslands = true;
|
|
1495
|
+
const { component } = use;
|
|
1496
|
+
const seeds = context.descriptions.get(block)?.seeds ?? [1];
|
|
1497
|
+
return {
|
|
1498
|
+
html: [
|
|
1499
|
+
`<div class="explainer-component" ${[
|
|
1500
|
+
`id="aufgabe-${key}"`,
|
|
1501
|
+
`data-component="${component.name}"`,
|
|
1502
|
+
`data-exercise data-assessment data-seeds="${JSON.stringify(seeds)}"`,
|
|
1503
|
+
...component.checked ? ["data-checked"] : []
|
|
1504
|
+
].join(" ")}>`,
|
|
1505
|
+
`<div class="explainer-island" hidden>${context.islands.get(block)?.get(context.lang) ?? ""}</div>`,
|
|
1506
|
+
`<script type="application/json" class="explainer-component-props">${json(propsIn(block.props, component, context.lang))}<\/script>`,
|
|
1507
|
+
`</div>`
|
|
1508
|
+
].join("\n"),
|
|
1509
|
+
seeds: [...seeds]
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* What a KI tutor is, on the start page: a section of its own that says what the tutor does and where it shows, the
|
|
1514
|
+
* choice of the learner's assistant ("Keiner" first; shown by the client module, which keeps it on the device), that the
|
|
1515
|
+
* learner uses an account of their own and the assistant is no offer of lernapps.net, and that nothing leaves the app
|
|
1516
|
+
* before a click. It starts no tutor itself.
|
|
1517
|
+
*/
|
|
1518
|
+
function tutorAbout(context) {
|
|
1519
|
+
const { site } = context;
|
|
1520
|
+
return [
|
|
1521
|
+
`<section class="explainer-tutor-about" aria-labelledby="ki-tutor">`,
|
|
1522
|
+
`<h2 id="ki-tutor">${plain(context.t("tutorTitle"))}</h2>`,
|
|
1523
|
+
`<p>${plain(context.t("tutorAbout"))}</p>`,
|
|
1524
|
+
`<p>${plain(context.t("tutorAboutStart"))}</p>`,
|
|
1525
|
+
`<p class="explainer-tutor-choice" hidden><label>${plain(context.t("tutorChoose"))} <select class="explainer-tutor-choose" data-storage="${plain(site.storage ?? "explainer")}"><option value="">${plain(context.t("tutorNone"))}</option>${wayOptions(context)}</select></label> <button type="button" class="explainer-tutor-needs-open" aria-haspopup="dialog" aria-label="${plain(context.t("needsOpen"))}" title="${plain(context.t("needsOpen"))}">${SETTINGS_ICON}</button></p>`,
|
|
1526
|
+
`<p class="explainer-tutor-hint" role="status" aria-live="polite" hidden></p>`,
|
|
1527
|
+
`<p>${plain(context.t("tutorAccount"))}</p>`,
|
|
1528
|
+
`<p>${plain(context.t("tutorAboutPrivacy"))}</p>`,
|
|
1529
|
+
`</section>`,
|
|
1530
|
+
needsChoice(context)
|
|
1531
|
+
].join("\n");
|
|
1532
|
+
}
|
|
1533
|
+
/** The symbol of "Einstellungen", which opens the dialog of the learner's needs: a gear, its name in aria-label. */
|
|
1534
|
+
const SETTINGS_ICON = "<svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z\"/></svg>";
|
|
1535
|
+
/** The name and the description of each need on the start page. */
|
|
1536
|
+
const NEED_TEXTS = {
|
|
1537
|
+
"kurze-schritte": ["needKurzeSchritte", "needKurzeSchritteAbout"],
|
|
1538
|
+
"wenig-lesen": ["needWenigLesen", "needWenigLesenAbout"],
|
|
1539
|
+
"mehr-tiefe": ["needMehrTiefe", "needMehrTiefeAbout"],
|
|
1540
|
+
"andere-muttersprache": ["needAndereMuttersprache", "needAndereMutterspracheAbout"],
|
|
1541
|
+
"klar-und-woertlich": ["needKlarUndWoertlich", "needKlarUndWoertlichAbout"],
|
|
1542
|
+
"rechnen-anschaulich": ["needRechnenAnschaulich", "needRechnenAnschaulichAbout"]
|
|
1543
|
+
};
|
|
1544
|
+
/**
|
|
1545
|
+
* The choice of the learner's needs for the tutor, on the start page (dec-tutor-needs): a dialog that "Einstellungen"
|
|
1546
|
+
* beside the choice of the assistant opens, with a box per need and one or two sentences on what it does, the first
|
|
1547
|
+
* language below `andere-muttersprache`, and whether the choice holds for every explainer or for this app only. The
|
|
1548
|
+
* tutor needs JavaScript, so the dialog does too: it stays in a <template> until the client module puts it into the
|
|
1549
|
+
* page, opens it and keeps the choice on the device.
|
|
1550
|
+
*/
|
|
1551
|
+
function needsChoice(context) {
|
|
1552
|
+
const { site, lang } = context;
|
|
1553
|
+
const languages = [...LANGUAGES].map((tag) => ({
|
|
1554
|
+
tag,
|
|
1555
|
+
name: languageName(tag, lang)
|
|
1556
|
+
})).sort((a, b) => a.name.localeCompare(b.name, lang));
|
|
1557
|
+
const language = [
|
|
1558
|
+
`<p class="explainer-tutor-language-choice" hidden><label>${plain(context.t("needsLanguage"))} <select class="explainer-tutor-language">`,
|
|
1559
|
+
`<option value="">${plain(context.t("needsLanguageNone"))}</option>`,
|
|
1560
|
+
...languages.map(({ tag, name }) => `<option value="${plain(tag)}">${plain(name)}</option>`),
|
|
1561
|
+
`</select></label></p>`
|
|
1562
|
+
].join("");
|
|
1563
|
+
const needs = NEEDS.map((need) => {
|
|
1564
|
+
const [name, about] = NEED_TEXTS[need];
|
|
1565
|
+
const id = `bedarf-${need}`;
|
|
1566
|
+
return [
|
|
1567
|
+
`<div class="explainer-tutor-need-choice"><input type="checkbox" class="explainer-tutor-need" id="${id}" value="${need}" aria-describedby="${id}-text" />`,
|
|
1568
|
+
`<label for="${id}">${plain(context.t(name))}</label>`,
|
|
1569
|
+
`<p class="explainer-tutor-need-text" id="${id}-text">${plain(context.t(about))}</p></div>`,
|
|
1570
|
+
need === "andere-muttersprache" ? language : ""
|
|
1571
|
+
].join("");
|
|
1572
|
+
});
|
|
1573
|
+
const scope = (value, key) => `<label><input type="radio" class="explainer-tutor-needs-scope" name="bedarf-gilt" value="${value}" /> ${plain(context.t(key))}</label>`;
|
|
1574
|
+
return [
|
|
1575
|
+
`<template class="explainer-tutor-needs-template">`,
|
|
1576
|
+
`<dialog class="explainer-tutor-needs" data-storage="${plain(site.storage ?? "explainer")}" aria-labelledby="bedarf-titel">`,
|
|
1577
|
+
`<form method="dialog">`,
|
|
1578
|
+
`<h2 id="bedarf-titel">${plain(context.t("needsTitle"))}</h2>`,
|
|
1579
|
+
`<p>${plain(context.t("needsAbout"))}</p>`,
|
|
1580
|
+
...needs,
|
|
1581
|
+
`<fieldset class="explainer-tutor-needs-where"><legend>${plain(context.t("needsScopeTitle"))}</legend>${scope("archetype", "needsScopeAll")} ${scope("app", "needsScopeApp")}</fieldset>`,
|
|
1582
|
+
`<p class="explainer-tutor-note">${plain(context.t("needsPrivacy"))}</p>`,
|
|
1583
|
+
`<p><button class="explainer-tutor-needs-done">${plain(context.t("needsDone"))}</button></p>`,
|
|
1584
|
+
`</form>`,
|
|
1585
|
+
`</dialog>`,
|
|
1586
|
+
`</template>`
|
|
1587
|
+
].join("\n");
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* The assistants of the app as options of a select, each by its name, and copying the prompt last (COPY). An assistant
|
|
1591
|
+
* that needs pasting (`paste`) carries its explanation in data-hint, which the client module shows once it is chosen.
|
|
1592
|
+
*/
|
|
1593
|
+
function wayOptions(context) {
|
|
1594
|
+
return [...PROVIDERS.map((provider) => {
|
|
1595
|
+
const hint = provider.paste === true ? ` data-hint="${plain(context.t("tutorPasteHint", { name: provider.name }))}"` : "";
|
|
1596
|
+
return `<option value="${plain(provider.name)}"${hint}>${plain(provider.name)}</option>`;
|
|
1597
|
+
}), `<option value="copy">${plain(context.t("tutorOther"))}</option>`].join("");
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* "Mit einem KI-Tutor lernen", way A to the tutor, on each content page alone, beside its title. Folded while no
|
|
1601
|
+
* assistant is switched on: unfolded, a select of the app's assistants and copying the text, "Einschalten" (hidden
|
|
1602
|
+
* until the client module runs; without it a plain link per assistant), that the learner uses an account of their own
|
|
1603
|
+
* and the assistant is no offer of lernapps.net, and the link to the page's tutor page. Once an assistant is on
|
|
1604
|
+
* (chosen here or on the start page, kept on the device), a bar in its place: "Mit <name> lernen"; it is changed or
|
|
1605
|
+
* switched off on the start page. Then a status line, and what the client module needs (prompt.ts): the start prompt
|
|
1606
|
+
* with BASE before its address, the page's tutor page and its Markdown, and the page's id, title, exercises and
|
|
1607
|
+
* sections, for the learner's position (position.ts), the result line of "Zurück" (result.ts) and the lines of
|
|
1608
|
+
* "Hilfe" (stuck.ts), whose button the client module adds to each exercise with the way chosen here.
|
|
1609
|
+
*/
|
|
1610
|
+
function tutorSection(context) {
|
|
1611
|
+
const { site, lang, route } = context;
|
|
1612
|
+
const plainLinks = PROVIDERS.map((provider) => `<a class="explainer-tutor-provider" href="${plain(plainAddress(provider.url))}" rel="noopener" target="_blank">${plain(context.t("tutorOpenWith", { name: provider.name }))}</a>`).join(" ");
|
|
1613
|
+
const { page } = route;
|
|
1614
|
+
const data = {
|
|
1615
|
+
start: startPrompt(route),
|
|
1616
|
+
base: BASE,
|
|
1617
|
+
query: QUERY,
|
|
1618
|
+
root: context.to(""),
|
|
1619
|
+
storage: site.storage ?? "explainer",
|
|
1620
|
+
...page === void 0 ? {} : { page: {
|
|
1621
|
+
id: page.id,
|
|
1622
|
+
title: inLanguage(page.title, lang).replace(/\s+/g, " ").trim(),
|
|
1623
|
+
exercises: pageExercises(site, page, context.descriptions).map((exercise) => ({
|
|
1624
|
+
id: exercise.id,
|
|
1625
|
+
...exercise.section === void 0 ? {} : { section: exercise.section },
|
|
1626
|
+
numbered: exercise.count > 0
|
|
1627
|
+
})),
|
|
1628
|
+
sections: sectionsOf(page)
|
|
1629
|
+
} },
|
|
1630
|
+
...page === void 0 ? {} : {
|
|
1631
|
+
tutor: tutorPageOf(page),
|
|
1632
|
+
markdown: `${tutorPageOf(page)}${TUTOR_MARKDOWN}`
|
|
1633
|
+
},
|
|
1634
|
+
ways: PROVIDERS.map((provider) => ({
|
|
1635
|
+
name: provider.name,
|
|
1636
|
+
url: provider.url,
|
|
1637
|
+
...provider.paste === true ? { paste: true } : {}
|
|
1638
|
+
})),
|
|
1639
|
+
placeholder: placeholderPrompt(lang),
|
|
1640
|
+
go: context.t("tutorGo"),
|
|
1641
|
+
done: context.t("tutorDone"),
|
|
1642
|
+
copy: context.t("tutorCopy"),
|
|
1643
|
+
copied: context.t("tutorCopied"),
|
|
1644
|
+
failed: context.t("tutorCopyFailed")
|
|
1645
|
+
};
|
|
1646
|
+
const tutorPage = page === void 0 ? "" : `<p><a class="explainer-tutor-guide" href="${context.to(tutorPageOf(page))}">${plain(context.t("tutorPageLink"))}</a></p>`;
|
|
1647
|
+
return [
|
|
1648
|
+
`<div class="explainer-tutor explainer-tutor-page">`,
|
|
1649
|
+
`<details class="explainer-tutor-off">`,
|
|
1650
|
+
`<summary>${plain(context.t("tutorTitle"))}</summary>`,
|
|
1651
|
+
`<p>${plain(context.t("tutorPageIntro"))}</p>`,
|
|
1652
|
+
`<p class="explainer-tutor-actions"><span class="explainer-tutor-way" hidden><select class="explainer-tutor-select" aria-label="${plain(context.t("tutorWay"))}">${wayOptions(context)}</select> <button type="button" class="explainer-tutor-on">${plain(context.t("tutorOn"))}</button></span> <span class="explainer-tutor-plain">${plainLinks}</span></p>`,
|
|
1653
|
+
`<p class="explainer-tutor-hint" role="status" aria-live="polite" hidden></p>`,
|
|
1654
|
+
`<p class="explainer-tutor-note">${plain(context.t("tutorAccount"))} ${plain(context.t("tutorScope"))}</p>`,
|
|
1655
|
+
tutorPage,
|
|
1656
|
+
`</details>`,
|
|
1657
|
+
`<p class="explainer-tutor-bar" hidden><button type="button" class="explainer-tutor-go"></button></p>`,
|
|
1658
|
+
`<p class="explainer-tutor-status" role="status" aria-live="polite"></p>`,
|
|
1659
|
+
`<script type="application/json" class="explainer-tutor-prompt">${json(data)}<\/script>`,
|
|
1660
|
+
`</div>`
|
|
1661
|
+
].filter((part) => part !== "").join("\n");
|
|
1662
|
+
}
|
|
1663
|
+
function head(context) {
|
|
1664
|
+
const { route, site } = context;
|
|
1665
|
+
const app = inLanguage(site.app.title, context.lang);
|
|
1666
|
+
const title = route.kind === "start" ? app : `${route.title} – ${app}`;
|
|
1667
|
+
const description = route.page?.description !== void 0 ? inLanguage(route.page.description, context.lang) : route.chapter?.summary !== void 0 && route.kind === "chapter" ? inLanguage(route.chapter.summary, context.lang) : inLanguage(site.app.intro, context.lang);
|
|
1668
|
+
const alternates = alternateLinks({
|
|
1669
|
+
...route,
|
|
1670
|
+
languages: site.app.languages
|
|
1671
|
+
}).join("\n ");
|
|
1672
|
+
return [
|
|
1673
|
+
`<title>${plain(title)}</title>`,
|
|
1674
|
+
`<meta name="description" content="${plain(description.replace(/\s+/g, " ").trim())}" />`,
|
|
1675
|
+
`<meta name="lernapps-archetype" content="explainer" />`,
|
|
1676
|
+
alternates
|
|
1677
|
+
].join("\n ");
|
|
1678
|
+
}
|
|
1679
|
+
function footer(context) {
|
|
1680
|
+
return pageFooter({
|
|
1681
|
+
footer: plain(context.t("footer")),
|
|
1682
|
+
imprint: plain(context.t("imprint")),
|
|
1683
|
+
privacy: plain(context.t("privacy"))
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
/** One page of the app, with the markup of its components as the build rendered them. */
|
|
1687
|
+
function renderPage(site, route, islands = /* @__PURE__ */ new Map(), descriptions = /* @__PURE__ */ new Map()) {
|
|
1688
|
+
const context = new Context(site, route, islands, descriptions);
|
|
1689
|
+
const body = [
|
|
1690
|
+
header(context),
|
|
1691
|
+
breadcrumbs(context),
|
|
1692
|
+
`<main class="explainer" id="inhalt-der-seite">`,
|
|
1693
|
+
main(context),
|
|
1694
|
+
`</main>`,
|
|
1695
|
+
pager(context),
|
|
1696
|
+
footer(context)
|
|
1697
|
+
].filter((part) => part !== "").join("\n");
|
|
1698
|
+
return {
|
|
1699
|
+
route,
|
|
1700
|
+
head: head(context),
|
|
1701
|
+
body,
|
|
1702
|
+
islands: context.hasIslands
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
/** Every page of the app, in every language. */
|
|
1706
|
+
function renderSite(site, islands = /* @__PURE__ */ new Map(), descriptions = /* @__PURE__ */ new Map()) {
|
|
1707
|
+
return routes(site).map((route) => renderPage(site, route, islands, descriptions));
|
|
1708
|
+
}
|
|
1709
|
+
// runtime/explainer/scope.ts
|
|
1710
|
+
/** The folder of a scope, below the app's root, a chapter's folder or a page's folder in each language tree. */
|
|
1711
|
+
const SCOPE_PAGE = "scope/";
|
|
1712
|
+
/** The words of a scope by language: its closing part and the links one level up. */
|
|
1713
|
+
const WORDS = {
|
|
1714
|
+
de: {
|
|
1715
|
+
out: "Gehört nicht dazu",
|
|
1716
|
+
chapter: "Alle Seiten des Kapitels: [Scope des Kapitels](../../scope/)",
|
|
1717
|
+
app: "Die ganze App: [Scope der App](../../scope/)"
|
|
1718
|
+
},
|
|
1719
|
+
en: {
|
|
1720
|
+
out: "Out of scope",
|
|
1721
|
+
chapter: "All pages of the chapter: [scope of the chapter](../../scope/)",
|
|
1722
|
+
app: "The whole app: [scope of the app](../../scope/)"
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
/**
|
|
1726
|
+
* The scopes the build writes for `site` in `lang`, by their folder within the language tree: "" the whole,
|
|
1727
|
+
* "<chapter>/" a chapter's with its pages, "<chapter>/<page>/" a page's; each but the whole with a link one level up.
|
|
1728
|
+
* None when the app has no scope.
|
|
1729
|
+
*/
|
|
1730
|
+
function scopeFiles(site, lang) {
|
|
1731
|
+
const files = /* @__PURE__ */ new Map();
|
|
1732
|
+
const { scope } = site.app;
|
|
1733
|
+
if (scope === void 0) return files;
|
|
1734
|
+
const words = lang === "de" ? WORDS.de : WORDS.en;
|
|
1735
|
+
const own = (text) => text === void 0 ? "" : inLanguage(text, lang).trim();
|
|
1736
|
+
const closing = [`## ${words.out}`, scope.out.map((item) => `- ${own(item)}`).join("\n")];
|
|
1737
|
+
const join = (...parts) => `${parts.filter((part) => part !== "").join("\n\n")}\n`;
|
|
1738
|
+
const whole = [`# ${own(site.app.title)}`, own(scope.text)];
|
|
1739
|
+
for (const chapter of site.chapters) {
|
|
1740
|
+
const pages = chapter.pages.map((page) => ({
|
|
1741
|
+
page,
|
|
1742
|
+
title: own(page.title),
|
|
1743
|
+
text: own(page.scope)
|
|
1744
|
+
}));
|
|
1745
|
+
whole.push(`## ${own(chapter.title)}`, own(chapter.scope));
|
|
1746
|
+
for (const { title, text } of pages) whole.push(`### ${title}`, text);
|
|
1747
|
+
files.set(`${chapter.id}/`, join(`# ${own(chapter.title)}`, own(chapter.scope), ...pages.flatMap(({ title, text }) => [`## ${title}`, text]), ...closing, words.app));
|
|
1748
|
+
for (const { page, title, text } of pages) files.set(`${chapter.id}/${page.id}/`, join(`# ${title}`, text, ...closing, words.chapter));
|
|
1749
|
+
}
|
|
1750
|
+
files.set("", join(...whole, ...closing));
|
|
1751
|
+
return files;
|
|
1752
|
+
}
|
|
1753
|
+
/** The page of the scope in `folder` (as scopeFiles names it) in `lang`: its Markdown rendered, for no menu and no search engine. */
|
|
1754
|
+
function scopeDocument(markdown, title, lang, folder = "") {
|
|
1755
|
+
const depth = folder.split("/").filter((segment) => segment !== "").length + 1;
|
|
1756
|
+
const body = renderMarkdown(markdown, (href) => href.startsWith("/") ? `${"../".repeat(depth)}${href.slice(1)}` : href);
|
|
1757
|
+
return [
|
|
1758
|
+
"<!doctype html>",
|
|
1759
|
+
`<html lang="${escape(lang)}">`,
|
|
1760
|
+
"<head>",
|
|
1761
|
+
"<meta charset=\"utf-8\">",
|
|
1762
|
+
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
|
|
1763
|
+
"<meta name=\"robots\" content=\"noindex\">",
|
|
1764
|
+
`<title>${escape(title)}</title>`,
|
|
1765
|
+
"</head>",
|
|
1766
|
+
`<body><main>${body}</main></body>`,
|
|
1767
|
+
"</html>"
|
|
1768
|
+
].join("\n");
|
|
1769
|
+
}
|
|
1770
|
+
// runtime/explainer/plugin.ts
|
|
1771
|
+
// @vitejs/plugin-vue and resolves vue to the copy this runtime depends on, so that the components and the islands
|
|
1772
|
+
const MARKER = "<!-- explainer -->";
|
|
1773
|
+
const ISLANDS = "\0lernapps-explainer-islands";
|
|
1774
|
+
/** The shell with the page in it, and `head` (the islands module) at the end of its head. */
|
|
1775
|
+
function fill(shell, page, head = "") {
|
|
1776
|
+
if (!shell.includes(MARKER)) throw new Error(`index.html has no ${MARKER}: put it in <body>, where the pages go`);
|
|
1777
|
+
const html = withLanguage(shell, page.route.lang).replace(/\s*<title>[\s\S]*?<\/title>/, "").replace("</head>", ` ${page.head}${head === "" ? "" : `\n ${head}`}\n </head>`);
|
|
1778
|
+
return fromDepth(html, page.route.path).replace(MARKER, () => page.body);
|
|
1779
|
+
}
|
|
1780
|
+
/** The islands module, next to this file: islands.ts in the source, explainer-islands.mjs in dist/. */
|
|
1781
|
+
const here = fileURLToPath(import.meta.url);
|
|
1782
|
+
const ISLANDS_RUNTIME = here.endsWith(".ts") ? join(dirname(here), "islands.ts") : join(dirname(here), "explainer-islands.mjs");
|
|
1783
|
+
/** Whether the app in `root` has components. */
|
|
1784
|
+
const hasComponents = (root) => {
|
|
1785
|
+
const dir = join(root, "src/components");
|
|
1786
|
+
return existsSync(dir) && readdirSync(dir).some((file) => file.endsWith(".vue"));
|
|
1787
|
+
};
|
|
1788
|
+
/** @vitejs/plugin-vue, loaded only when the app has components: an app without them loads nothing of Vue. */
|
|
1789
|
+
function vueWhenComponents() {
|
|
1790
|
+
let inner;
|
|
1791
|
+
const delegate = (name) => function(...args) {
|
|
1792
|
+
const hook = inner?.[name];
|
|
1793
|
+
if (hook === void 0) return void 0;
|
|
1794
|
+
return (typeof hook === "function" ? hook : hook.handler).apply(this, args);
|
|
1795
|
+
};
|
|
1796
|
+
return {
|
|
1797
|
+
name: "lernapps-explainer-vue",
|
|
1798
|
+
...Object.fromEntries([
|
|
1799
|
+
"configResolved",
|
|
1800
|
+
"options",
|
|
1801
|
+
"buildStart",
|
|
1802
|
+
"configureServer",
|
|
1803
|
+
"handleHotUpdate",
|
|
1804
|
+
"shouldTransformCachedModule",
|
|
1805
|
+
"resolveId",
|
|
1806
|
+
"load",
|
|
1807
|
+
"transform"
|
|
1808
|
+
].map((name) => [name, delegate(name)])),
|
|
1809
|
+
async config(config, env) {
|
|
1810
|
+
if (!hasComponents(resolve(config.root ?? process.cwd()))) return void 0;
|
|
1811
|
+
const { default: vue } = await import("@vitejs/plugin-vue");
|
|
1812
|
+
inner = vue(VUE_OPTIONS);
|
|
1813
|
+
const own = await delegate("config").call(this, config, env) ?? {};
|
|
1814
|
+
return {
|
|
1815
|
+
...own,
|
|
1816
|
+
resolve: {
|
|
1817
|
+
...own.resolve,
|
|
1818
|
+
alias: vueAlias()
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
const reason = (error) => (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
|
|
1825
|
+
/**
|
|
1826
|
+
* The property test of each exercise component's logic with each block's props (`test`), what its logic describes
|
|
1827
|
+
* (about, and describe for each task number in each language), and the markup of each block's component in each
|
|
1828
|
+
* language, rendered by Vue's server renderer, an exercise component with the task number 1; or the problems, one
|
|
1829
|
+
* line each.
|
|
1830
|
+
*/
|
|
1831
|
+
async function renderComponents(site, load, test) {
|
|
1832
|
+
const problems = [];
|
|
1833
|
+
const islands = /* @__PURE__ */ new Map();
|
|
1834
|
+
const descriptions = /* @__PURE__ */ new Map();
|
|
1835
|
+
if (site.components.length === 0) return {
|
|
1836
|
+
islands,
|
|
1837
|
+
descriptions,
|
|
1838
|
+
problems
|
|
1839
|
+
};
|
|
1840
|
+
const { createSSRApp } = await load("vue");
|
|
1841
|
+
const { renderToString } = await load("vue/server-renderer");
|
|
1842
|
+
for (const { block, component, place, examples } of site.components) {
|
|
1843
|
+
if (component.logic !== void 0) {
|
|
1844
|
+
const logic = await load(`/${component.file.replace(/\.vue$/, ".ts")}`);
|
|
1845
|
+
const props = propsIn(block.props, component, "de");
|
|
1846
|
+
const contract = test ? propertyProblems(logic, props) : [];
|
|
1847
|
+
for (const problem of contract) problems.push(`${place}: ${component.name}.ts: ${problem} (vue-component-exercise-contract)`);
|
|
1848
|
+
if (test && contract.length === 0) for (const { kind, index, message } of exampleProblems(logic, props, block.examples)) problems.push(`${examples[kind][index] ?? place}: ${component.name}.ts: ${message} (vue-component-exercise-contract)`);
|
|
1849
|
+
if (contract.length === 0) {
|
|
1850
|
+
const described = describedProblems(logic, (lang) => propsIn(block.props, component, lang), site.app.languages);
|
|
1851
|
+
for (const problem of described.problems) problems.push(`${place}: ${component.name}.ts: ${problem} (vue-component-exercise-contract)`);
|
|
1852
|
+
if (described.description !== void 0) descriptions.set(block, described.description);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
const definition = (await load(`/${component.file}`))["default"];
|
|
1856
|
+
const markup = /* @__PURE__ */ new Map();
|
|
1857
|
+
for (const lang of site.app.languages) {
|
|
1858
|
+
const app = createSSRApp(definition, {
|
|
1859
|
+
...propsIn(block.props, component, lang),
|
|
1860
|
+
...component.exercise ? { seed: descriptions.get(block)?.seeds[0] ?? 1 } : {},
|
|
1861
|
+
...component.checked ? { checked: false } : {}
|
|
1862
|
+
});
|
|
1863
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
1864
|
+
app.config.warnHandler = (message) => warnings.add(message);
|
|
1865
|
+
try {
|
|
1866
|
+
markup.set(lang, await renderToString(app));
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
problems.push(`${place} (${lang}): ${component.name} throws while the build renders it: ${reason(error)}`);
|
|
1869
|
+
}
|
|
1870
|
+
for (const warning of warnings) problems.push(`${place} (${lang}): Vue warns while the build renders ${component.name}: ${warning}`);
|
|
1871
|
+
}
|
|
1872
|
+
islands.set(block, markup);
|
|
1873
|
+
}
|
|
1874
|
+
return {
|
|
1875
|
+
islands,
|
|
1876
|
+
descriptions,
|
|
1877
|
+
problems
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* The islands module of the app: the islands runtime, with each component the pages use and the logic module of each
|
|
1882
|
+
* exercise component, which checks its answers in the browser, loaded on first use.
|
|
1883
|
+
*/
|
|
1884
|
+
function islandsModule(site) {
|
|
1885
|
+
const used = new Map(site.components.map((use) => [use.component.name, use.component]));
|
|
1886
|
+
const loader = (name, file) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(file)}),`;
|
|
1887
|
+
return [
|
|
1888
|
+
`import { startIslands } from ${JSON.stringify(ISLANDS_RUNTIME)};`,
|
|
1889
|
+
`startIslands({`,
|
|
1890
|
+
...[...used.keys()].map((name) => loader(name, `/src/components/${name}.vue`)),
|
|
1891
|
+
`}, {`,
|
|
1892
|
+
...[...used.values()].filter((component) => component.exercise).map((component) => loader(component.name, `/src/components/${component.name}.ts`)),
|
|
1893
|
+
`});`,
|
|
1894
|
+
""
|
|
1895
|
+
].join("\n");
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* What the build writes for a tutor beside the pages: the tutor page of each content page and of each self-assessment,
|
|
1899
|
+
* by its folder from the app's root (with the language root), its HTML as index.html and its Markdown beside it.
|
|
1900
|
+
*/
|
|
1901
|
+
function tutorAssets(site, descriptions) {
|
|
1902
|
+
const pages = /* @__PURE__ */ new Map();
|
|
1903
|
+
for (const lang of site.app.languages) {
|
|
1904
|
+
for (const page of contentPages(site)) {
|
|
1905
|
+
const { markdown, cut } = tutorPage(site, page, lang, descriptions);
|
|
1906
|
+
pages.set(`${languageRoot(lang)}${tutorPageOf(page)}`, {
|
|
1907
|
+
markdown,
|
|
1908
|
+
html: tutorPageDocument(markdown, site, page, lang),
|
|
1909
|
+
cut
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
for (const assessment of assessments(site)) {
|
|
1913
|
+
const markdown = assessmentTutorMarkdown(site, assessment, lang);
|
|
1914
|
+
pages.set(`${languageRoot(lang)}${assessmentTutorOf(assessment)}`, {
|
|
1915
|
+
markdown,
|
|
1916
|
+
html: assessmentTutorDocument(markdown, site, lang),
|
|
1917
|
+
cut: []
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return pages;
|
|
1922
|
+
}
|
|
1923
|
+
/** The build step of an explainer: validates the content in src/ and writes every page. */
|
|
1924
|
+
function explainer() {
|
|
1925
|
+
let root = process.cwd();
|
|
1926
|
+
let command = "build";
|
|
1927
|
+
let site;
|
|
1928
|
+
let islands = /* @__PURE__ */ new Map();
|
|
1929
|
+
let descriptions = /* @__PURE__ */ new Map();
|
|
1930
|
+
let islandsChunk;
|
|
1931
|
+
let devServer;
|
|
1932
|
+
const load = () => site ??= loadSite(root);
|
|
1933
|
+
return [vueWhenComponents(), {
|
|
1934
|
+
name: "lernapps-explainer",
|
|
1935
|
+
enforce: "post",
|
|
1936
|
+
configResolved(config) {
|
|
1937
|
+
root = config.root;
|
|
1938
|
+
command = config.command;
|
|
1939
|
+
},
|
|
1940
|
+
async buildStart() {
|
|
1941
|
+
site = void 0;
|
|
1942
|
+
islands = /* @__PURE__ */ new Map();
|
|
1943
|
+
descriptions = /* @__PURE__ */ new Map();
|
|
1944
|
+
islandsChunk = void 0;
|
|
1945
|
+
let content;
|
|
1946
|
+
try {
|
|
1947
|
+
content = load();
|
|
1948
|
+
} catch (error) {
|
|
1949
|
+
this.error(error instanceof Error ? error.message : String(error));
|
|
1950
|
+
}
|
|
1951
|
+
if (command !== "build" || content.components.length === 0) return;
|
|
1952
|
+
const rendered = await withServer(root, (loader) => renderComponents(content, loader, true));
|
|
1953
|
+
if (rendered.problems.length > 0) this.error(new ContentError(rendered.problems).message);
|
|
1954
|
+
islands = rendered.islands;
|
|
1955
|
+
descriptions = rendered.descriptions;
|
|
1956
|
+
islandsChunk = this.emitFile({
|
|
1957
|
+
type: "chunk",
|
|
1958
|
+
id: ISLANDS,
|
|
1959
|
+
name: "islands"
|
|
1960
|
+
});
|
|
1961
|
+
},
|
|
1962
|
+
resolveId(id) {
|
|
1963
|
+
return id === ISLANDS ? ISLANDS : void 0;
|
|
1964
|
+
},
|
|
1965
|
+
load(id) {
|
|
1966
|
+
return id === ISLANDS ? islandsModule(load()) : void 0;
|
|
1967
|
+
},
|
|
1968
|
+
generateBundle(_options, bundle) {
|
|
1969
|
+
const index = bundle["index.html"];
|
|
1970
|
+
if (index === void 0 || index.type !== "asset") return;
|
|
1971
|
+
const shell = String(index.source);
|
|
1972
|
+
const content = load();
|
|
1973
|
+
const script = islandsChunk === void 0 ? "" : `<script type="module" crossorigin src="./${this.getFileName(islandsChunk)}"><\/script>`;
|
|
1974
|
+
for (const page of renderSite(content, islands, descriptions)) {
|
|
1975
|
+
const html = fill(shell, page, page.islands ? script : "");
|
|
1976
|
+
if (page.route.path === "") index.source = html;
|
|
1977
|
+
else this.emitFile({
|
|
1978
|
+
type: "asset",
|
|
1979
|
+
fileName: `${page.route.path}index.html`,
|
|
1980
|
+
source: html
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
this.emitFile({
|
|
1984
|
+
type: "asset",
|
|
1985
|
+
fileName: "404.html",
|
|
1986
|
+
source: notFoundPage()
|
|
1987
|
+
});
|
|
1988
|
+
for (const [image, file] of content.images) this.emitFile({
|
|
1989
|
+
type: "asset",
|
|
1990
|
+
fileName: image,
|
|
1991
|
+
source: readFileSync(file)
|
|
1992
|
+
});
|
|
1993
|
+
const problems = [];
|
|
1994
|
+
const tutor = tutorAssets(content, descriptions);
|
|
1995
|
+
for (const lang of content.app.languages) {
|
|
1996
|
+
const files = /* @__PURE__ */ new Set([
|
|
1997
|
+
...routes(content).filter((route) => route.lang === lang).map((route) => route.local),
|
|
1998
|
+
...contentPages(content).map((page) => tutorPageOf(page)),
|
|
1999
|
+
...assessments(content).map((assessment) => assessmentTutorOf(assessment)),
|
|
2000
|
+
...[...scopeFiles(content, lang).keys()].map((folder) => `${folder}${SCOPE_PAGE}`)
|
|
2001
|
+
]);
|
|
2002
|
+
const anchors = new Map(contentPages(content).map((page) => [`${page.chapter}/${page.id}/`, anchorsOf(content, page)]));
|
|
2003
|
+
for (const local of [...contentPages(content).map((contentPage) => tutorPageOf(contentPage)), ...assessments(content).map((assessment) => assessmentTutorOf(assessment))]) {
|
|
2004
|
+
const page = tutor.get(`${languageRoot(lang)}${local}`);
|
|
2005
|
+
if (page === void 0) continue;
|
|
2006
|
+
problems.push(...linkProblems(local, page.markdown, files, anchors).map((problem) => `${languageRoot(lang)}${problem}`));
|
|
2007
|
+
this.emitFile({
|
|
2008
|
+
type: "asset",
|
|
2009
|
+
fileName: `${languageRoot(lang)}${local}index.html`,
|
|
2010
|
+
source: page.html
|
|
2011
|
+
});
|
|
2012
|
+
this.emitFile({
|
|
2013
|
+
type: "asset",
|
|
2014
|
+
fileName: `${languageRoot(lang)}${local}${TUTOR_MARKDOWN}`,
|
|
2015
|
+
source: page.markdown
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
for (const lang of content.app.languages) for (const [folder, markdown] of scopeFiles(content, lang)) {
|
|
2020
|
+
const at = `${languageRoot(lang)}${folder}${SCOPE_PAGE}`;
|
|
2021
|
+
this.emitFile({
|
|
2022
|
+
type: "asset",
|
|
2023
|
+
fileName: `${at}${TUTOR_MARKDOWN}`,
|
|
2024
|
+
source: markdown
|
|
2025
|
+
});
|
|
2026
|
+
this.emitFile({
|
|
2027
|
+
type: "asset",
|
|
2028
|
+
fileName: `${at}index.html`,
|
|
2029
|
+
source: scopeDocument(markdown, inLanguage(content.app.title, lang), lang, folder)
|
|
2030
|
+
});
|
|
2031
|
+
if (folder === "" && bytesOf(markdown) > 64e3) this.info(`lernapps explainer: the scope of the whole app (${languageRoot(lang)}${SCOPE_PAGE}) is larger than ${TUTOR_PAGE_BUDGET / 1e3} kB (${(bytesOf(markdown) / 1e3).toFixed(0)} kB), which a tutor may get cut off`);
|
|
2032
|
+
}
|
|
2033
|
+
const large = [...tutor].filter(([, page]) => bytesOf(page.html) > TUTOR_PAGE_BUDGET);
|
|
2034
|
+
if (large.length > 0) this.info(`lernapps explainer: ${large.length} tutor page${large.length === 1 ? " is" : "s are"} larger than ${TUTOR_PAGE_BUDGET / 1e3} kB, which a tutor may get cut off: ${large.map(([file, page]) => `${file} (${(bytesOf(page.html) / 1e3).toFixed(0)} kB)`).join(", ")}`);
|
|
2035
|
+
const cut = [...tutor].flatMap(([file, page]) => page.cut.map((one) => ({
|
|
2036
|
+
file,
|
|
2037
|
+
...one
|
|
2038
|
+
})));
|
|
2039
|
+
if (cut.length > 0) this.warn(`lernapps explainer: ${cut.length} exercise${cut.length === 1 ? " shows" : "s show"} only some of ${cut.length === 1 ? "its" : "their"} task numbers on the tutor page, to stay within ${TUTOR_PAGE_BUDGET / 1e3} kB; export template, params, mistakes and show from its logic module for a table of all its numbers: ${cut.map((one) => `${one.file} ${one.id} (${one.shown} of ${one.count})`).join(", ")}`);
|
|
2040
|
+
const hint = tableHint(content, descriptions);
|
|
2041
|
+
if (hint !== void 0) this.info(hint);
|
|
2042
|
+
const steps = stepHint(content);
|
|
2043
|
+
if (steps !== void 0) this.info(steps);
|
|
2044
|
+
const few = [...contentPages(content).flatMap((page) => [...allBlocks(page.blocks)].flatMap((block) => block.type === "generated" ? [[block.id, taskSeeds(block, dataOf(block, content.data)).length]] : [])), ...content.components.flatMap((use) => {
|
|
2045
|
+
const seeds = descriptions.get(use.block)?.seeds;
|
|
2046
|
+
return seeds === void 0 || use.id === void 0 ? [] : [[use.id, seeds.length]];
|
|
2047
|
+
})].filter(([, count]) => count < 67);
|
|
2048
|
+
if (few.length > 0) this.info(`lernapps explainer: ${few.length} exercise${few.length === 1 ? " has" : "s have"} fewer than 67 distinct tasks, numbered 1 to N: ${few.map(([id, count]) => `${id} (${count})`).join(", ")}`);
|
|
2049
|
+
const chunks = Object.values(bundle).flatMap((output) => output.type === "chunk" ? [output] : []);
|
|
2050
|
+
const folder = join(root, "src/components");
|
|
2051
|
+
for (const component of new Map(content.components.map((use) => [use.component.name, use.component])).values()) {
|
|
2052
|
+
const chunk = chunks.find((candidate) => candidate.facadeModuleId === component.path);
|
|
2053
|
+
if (chunk === void 0) continue;
|
|
2054
|
+
const own = [chunk.code, ...chunk.imports.map((file) => chunks.find((candidate) => candidate.fileName === file)).filter((imported) => imported !== void 0 && imported.moduleIds.every((id) => id.startsWith(folder))).map((imported) => imported?.code ?? "")].join("\n");
|
|
2055
|
+
const size = gzipSync(own).length;
|
|
2056
|
+
if (size > 2e4) problems.push(`${component.file}: ${(size / 1e3).toFixed(1)} kB gzip of its own code, more than the budget of ${SIZE_BUDGET / 1e3} kB; split it into smaller components, or ask for a built-in block (vue-component-size)`);
|
|
2057
|
+
}
|
|
2058
|
+
if (problems.length > 0) this.error(new ContentError(problems).message);
|
|
2059
|
+
},
|
|
2060
|
+
configureServer(server) {
|
|
2061
|
+
devServer = server;
|
|
2062
|
+
server.watcher.on("all", (_event, file) => {
|
|
2063
|
+
if (file.startsWith(join(root, "src")) && !file.endsWith(".ts")) {
|
|
2064
|
+
site = void 0;
|
|
2065
|
+
server.ws.send({ type: "full-reload" });
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
server.middlewares.use((request, response, next) => {
|
|
2069
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
2070
|
+
const path = decodeURIComponent(url.pathname).replace(/^\//, "");
|
|
2071
|
+
if (path.startsWith("images/")) {
|
|
2072
|
+
response.setHeader("content-type", imageType(path));
|
|
2073
|
+
response.end(readFileSync(join(root, "src", path)));
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
const forTutor = new RegExp(`(^|/)tutor/(${TUTOR_MARKDOWN.replace(".", "\\.")})?$`).test(path);
|
|
2077
|
+
const forScope = new RegExp(`(^|/)${SCOPE_PAGE}(${TUTOR_MARKDOWN.replace(".", "\\.")})?$`).test(path);
|
|
2078
|
+
if (!(path === "" || path.endsWith("/") || forTutor || forScope)) return next();
|
|
2079
|
+
const fail = (error) => {
|
|
2080
|
+
response.statusCode = 500;
|
|
2081
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
2082
|
+
response.end(error instanceof Error ? error.message : String(error));
|
|
2083
|
+
};
|
|
2084
|
+
let content;
|
|
2085
|
+
try {
|
|
2086
|
+
content = load();
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
fail(error);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const dev = devServer;
|
|
2092
|
+
if (dev === void 0) return next();
|
|
2093
|
+
renderComponents(content, (id) => dev.ssrLoadModule(id), false).then(({ islands: rendered, descriptions: described, problems }) => {
|
|
2094
|
+
if (problems.length > 0) throw new ContentError(problems);
|
|
2095
|
+
if (forScope) {
|
|
2096
|
+
const markdown = path.endsWith(TUTOR_MARKDOWN);
|
|
2097
|
+
const lang = content.app.languages.find((one) => languageRoot(one) !== "" && path.startsWith(languageRoot(one))) ?? content.app.languages[0] ?? "de";
|
|
2098
|
+
const folder = path.slice(languageRoot(lang).length).replace(new RegExp(`${SCOPE_PAGE}(${TUTOR_MARKDOWN.replace(".", "\\.")})?$`), "");
|
|
2099
|
+
const scope = scopeFiles(content, lang).get(folder);
|
|
2100
|
+
if (scope === void 0) return next();
|
|
2101
|
+
response.setHeader("content-type", `text/${markdown ? "markdown" : "html"}; charset=utf-8`);
|
|
2102
|
+
response.end(markdown ? scope : scopeDocument(scope, inLanguage(content.app.title, lang), lang, folder));
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (forTutor) {
|
|
2106
|
+
const markdown = path.endsWith(TUTOR_MARKDOWN);
|
|
2107
|
+
const page = tutorAssets(content, described).get(markdown ? path.slice(0, -8) : path);
|
|
2108
|
+
if (page === void 0) return next();
|
|
2109
|
+
response.setHeader("content-type", `text/${markdown ? "markdown" : "html"}; charset=utf-8`);
|
|
2110
|
+
response.end(markdown ? page.markdown : page.html);
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
const page = renderSite(content, rendered, described).find((candidate) => candidate.route.path === path);
|
|
2114
|
+
if (page === void 0) return next();
|
|
2115
|
+
const shell = readFileSync(join(root, "index.html"), "utf8");
|
|
2116
|
+
const head = page.islands ? `<script type="module" src="/@id/__x00__lernapps-explainer-islands"><\/script>` : "";
|
|
2117
|
+
return dev.transformIndexHtml(`/${path}`, fill(shell, {
|
|
2118
|
+
...page,
|
|
2119
|
+
route: {
|
|
2120
|
+
...page.route,
|
|
2121
|
+
path: ""
|
|
2122
|
+
}
|
|
2123
|
+
}, head)).then((html) => {
|
|
2124
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
2125
|
+
response.end(html);
|
|
2126
|
+
});
|
|
2127
|
+
}).catch(fail);
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
}];
|
|
2131
|
+
}
|
|
2132
|
+
export { explainer, fill };
|