@lernapps/app-templates 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +195 -2
  3. package/dist/appname-Bh23cUuz.mjs +12 -0
  4. package/dist/assessment-By2GGTJC.mjs +1696 -0
  5. package/dist/block-BK_ohsH1.mjs +82 -0
  6. package/dist/blocks-BmRrD7UY.mjs +149 -0
  7. package/dist/blocks.mjs +4 -0
  8. package/dist/check-BkEl-m-F.mjs +254 -0
  9. package/dist/content-cYBPYg_J.mjs +140 -0
  10. package/dist/contract-PONpxJtd.mjs +52 -0
  11. package/dist/e2e-BiMI-pCL.mjs +89 -0
  12. package/dist/engine-BqF2Vffn.mjs +136 -0
  13. package/dist/explainer-e2e.mjs +1297 -0
  14. package/dist/explainer-islands.mjs +197 -0
  15. package/dist/explainer-plugin.mjs +2131 -0
  16. package/dist/explainer.mjs +1246 -0
  17. package/dist/html-mffkHGHh.mjs +89 -0
  18. package/dist/image-CspJVfyQ.mjs +21 -0
  19. package/dist/interactive-e2e.mjs +140 -0
  20. package/dist/interactive-plugin.mjs +242 -0
  21. package/dist/interactive.mjs +75 -0
  22. package/dist/load-C-0hUaSC.mjs +16947 -0
  23. package/dist/needs-BzEahJ6X.mjs +97 -0
  24. package/dist/pages-9RaaomoO.mjs +81 -0
  25. package/dist/questions-e2e.mjs +1 -0
  26. package/dist/questions-engine.mjs +3 -0
  27. package/dist/questions.mjs +6 -0
  28. package/dist/quiz-BKc8JYtC.mjs +70 -0
  29. package/dist/quiz-e2e.mjs +174 -0
  30. package/dist/quiz-plugin.mjs +266 -0
  31. package/dist/quiz.mjs +246 -0
  32. package/dist/render-BJey0bwl.mjs +540 -0
  33. package/dist/render-BL6fV_NI.mjs +507 -0
  34. package/dist/render-DpRO_RKm.mjs +146 -0
  35. package/dist/result-_eQcamoz.mjs +180 -0
  36. package/dist/tasks-DADUYy52.mjs +74 -0
  37. package/dist/validate-COaZso7u.mjs +1072 -0
  38. package/dist/validate-CxtdeQ5S.mjs +146 -0
  39. package/dist/vue-DO1m6q1R.mjs +40 -0
  40. package/dist/vue-code-BkJjW_I_.mjs +320 -0
  41. package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
  42. package/docs/arc42/02-constraints.arc42.md +228 -0
  43. package/docs/arc42/03-system-context.arc42.md +118 -0
  44. package/docs/arc42/04-solution-strategy.arc42.md +47 -0
  45. package/docs/arc42/05-building-blocks.arc42.md +307 -0
  46. package/docs/arc42/06-runtime-view.arc42.md +98 -0
  47. package/docs/arc42/07-deployment-view.arc42.md +160 -0
  48. package/docs/arc42/08-concepts.arc42.md +123 -0
  49. package/docs/arc42/09-decisions.arc42.md +246 -0
  50. package/docs/arc42/10-quality-requirements.arc42.md +155 -0
  51. package/docs/arc42/11-risks.arc42.md +113 -0
  52. package/docs/arc42/12-glossary.arc42.md +162 -0
  53. package/docs/arc42/architecture-evidence.md +395 -0
  54. package/explainer/.arc42ignore +3 -0
  55. package/explainer/README.md +54 -0
  56. package/explainer/_gitignore +5 -0
  57. package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
  58. package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
  59. package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
  60. package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
  61. package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
  62. package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
  63. package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
  64. package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
  65. package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
  66. package/explainer/e2e/explainer.e2e.ts +3 -0
  67. package/explainer/index.html +11 -0
  68. package/explainer/package.json +18 -0
  69. package/explainer/playwright.config.ts +1 -0
  70. package/explainer/src/app.yaml +20 -0
  71. package/explainer/src/components/ProzentFeld.test.ts +30 -0
  72. package/explainer/src/components/ProzentFeld.ts +121 -0
  73. package/explainer/src/components/ProzentFeld.vue +84 -0
  74. package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
  75. package/explainer/src/main.ts +4 -0
  76. package/explainer/src/prozent/chapter.yaml +18 -0
  77. package/explainer/src/prozent/grundwert.yaml +115 -0
  78. package/explainer/src/prozent/prozentsatz.yaml +99 -0
  79. package/explainer/src/prozent/prozentwert.yaml +119 -0
  80. package/explainer/tsconfig.json +1 -0
  81. package/explainer/vite.config.ts +4 -0
  82. package/interactive/.arc42ignore +5 -0
  83. package/interactive/README.md +28 -0
  84. package/interactive/_gitignore +4 -0
  85. package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
  86. package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
  87. package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
  88. package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
  89. package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
  90. package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
  91. package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
  92. package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
  93. package/interactive/e2e/interactive.e2e.ts +3 -0
  94. package/interactive/index.html +11 -0
  95. package/interactive/package.json +18 -0
  96. package/interactive/playwright.config.ts +1 -0
  97. package/interactive/src/app.yaml +28 -0
  98. package/interactive/src/core/index.ts +101 -0
  99. package/interactive/src/data/lessons.json +4 -0
  100. package/interactive/src/env.d.ts +6 -0
  101. package/interactive/src/main.ts +6 -0
  102. package/interactive/src/messages/de.json +12 -0
  103. package/interactive/src/playthroughs/grundreihe.yaml +33 -0
  104. package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
  105. package/interactive/src/ui/App.vue +85 -0
  106. package/interactive/tsconfig.json +1 -0
  107. package/interactive/vite.config.ts +4 -0
  108. package/package.json +142 -4
  109. package/quiz/README.md +31 -0
  110. package/quiz/_gitignore +5 -0
  111. package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
  112. package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
  113. package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
  114. package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
  115. package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
  116. package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
  117. package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
  118. package/quiz/e2e/quiz.e2e.ts +3 -0
  119. package/quiz/index.html +11 -0
  120. package/quiz/package.json +18 -0
  121. package/quiz/playwright.config.ts +1 -0
  122. package/quiz/src/images/berge-ordnen.jpg +0 -0
  123. package/quiz/src/images/everest-hoehe.jpg +0 -0
  124. package/quiz/src/images/galapagos.jpg +0 -0
  125. package/quiz/src/images/grand-canyon.jpg +0 -0
  126. package/quiz/src/images/totes-meer.jpg +0 -0
  127. package/quiz/src/images/uluru.jpg +0 -0
  128. package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
  129. package/quiz/src/main.ts +4 -0
  130. package/quiz/src/quiz.json +312 -0
  131. package/quiz/tsconfig.json +1 -0
  132. package/quiz/vite.config.ts +4 -0
  133. package/runtime/explainer/assessment-client.ts +478 -0
  134. package/runtime/explainer/assessment-page.ts +446 -0
  135. package/runtime/explainer/assessment.ts +393 -0
  136. package/runtime/explainer/check-page.ts +520 -0
  137. package/runtime/explainer/components/exercise.ts +409 -0
  138. package/runtime/explainer/components/props.ts +230 -0
  139. package/runtime/explainer/components/read.ts +428 -0
  140. package/runtime/explainer/content.ts +277 -0
  141. package/runtime/explainer/e2e.ts +1474 -0
  142. package/runtime/explainer/exercises.ts +324 -0
  143. package/runtime/explainer/explainer.css +1283 -0
  144. package/runtime/explainer/explainer.ts +838 -0
  145. package/runtime/explainer/explainer.v1.schema.json +1165 -0
  146. package/runtime/explainer/expression.ts +407 -0
  147. package/runtime/explainer/fraction.ts +107 -0
  148. package/runtime/explainer/generated/comparator.ts +31 -0
  149. package/runtime/explainer/generated/question.ts +165 -0
  150. package/runtime/explainer/generated/soundness.ts +280 -0
  151. package/runtime/explainer/generated/texts.ts +257 -0
  152. package/runtime/explainer/generated/types.ts +103 -0
  153. package/runtime/explainer/generated/values.ts +136 -0
  154. package/runtime/explainer/islands.ts +281 -0
  155. package/runtime/explainer/load.ts +457 -0
  156. package/runtime/explainer/messages/de.json +200 -0
  157. package/runtime/explainer/messages/en.json +200 -0
  158. package/runtime/explainer/messages.ts +13 -0
  159. package/runtime/explainer/needs.ts +122 -0
  160. package/runtime/explainer/open-task.ts +235 -0
  161. package/runtime/explainer/outline.ts +299 -0
  162. package/runtime/explainer/plugin.ts +488 -0
  163. package/runtime/explainer/position.ts +62 -0
  164. package/runtime/explainer/prompt.ts +75 -0
  165. package/runtime/explainer/render.ts +686 -0
  166. package/runtime/explainer/result.ts +238 -0
  167. package/runtime/explainer/routes.ts +119 -0
  168. package/runtime/explainer/schema.ts +140 -0
  169. package/runtime/explainer/scope.ts +87 -0
  170. package/runtime/explainer/stuck.ts +57 -0
  171. package/runtime/explainer/tasks.ts +109 -0
  172. package/runtime/explainer/tutor.ts +469 -0
  173. package/runtime/explainer/tutorpage.ts +669 -0
  174. package/runtime/explainer/video.ts +123 -0
  175. package/runtime/interactive/contract.ts +121 -0
  176. package/runtime/interactive/e2e.ts +190 -0
  177. package/runtime/interactive/interactive.css +82 -0
  178. package/runtime/interactive/interactive.ts +115 -0
  179. package/runtime/interactive/interactive.v1.schema.json +146 -0
  180. package/runtime/interactive/load.ts +188 -0
  181. package/runtime/interactive/messages/de.json +14 -0
  182. package/runtime/interactive/messages/en.json +14 -0
  183. package/runtime/interactive/messages.ts +11 -0
  184. package/runtime/interactive/playthrough.v1.schema.json +143 -0
  185. package/runtime/interactive/plugin.ts +266 -0
  186. package/runtime/interactive/render.ts +126 -0
  187. package/runtime/quiz/e2e.ts +233 -0
  188. package/runtime/quiz/engine.ts +242 -0
  189. package/runtime/quiz/messages/de.json +23 -0
  190. package/runtime/quiz/messages/en.json +23 -0
  191. package/runtime/quiz/plugin.ts +150 -0
  192. package/runtime/quiz/quiz.css +154 -0
  193. package/runtime/quiz/quiz.ts +56 -0
  194. package/runtime/quiz/quiz.v1.schema.json +72 -0
  195. package/runtime/quiz/render.ts +173 -0
  196. package/runtime/quiz/validate.ts +108 -0
  197. package/runtime/shared/appname.ts +16 -0
  198. package/runtime/shared/blocks/block.ts +162 -0
  199. package/runtime/shared/blocks/blocks.css +190 -0
  200. package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
  201. package/runtime/shared/blocks/code.ts +26 -0
  202. package/runtime/shared/blocks/figures.ts +49 -0
  203. package/runtime/shared/blocks/image.ts +25 -0
  204. package/runtime/shared/blocks/index.ts +13 -0
  205. package/runtime/shared/blocks/messages/de.json +4 -0
  206. package/runtime/shared/blocks/messages/en.json +4 -0
  207. package/runtime/shared/blocks/render.ts +134 -0
  208. package/runtime/shared/blocks/validate.ts +165 -0
  209. package/runtime/shared/content.ts +197 -0
  210. package/runtime/shared/examples.ts +20 -0
  211. package/runtime/shared/html.ts +10 -0
  212. package/runtime/shared/languages.ts +94 -0
  213. package/runtime/shared/page.css +101 -0
  214. package/runtime/shared/pages.ts +114 -0
  215. package/runtime/shared/questions/e2e.ts +115 -0
  216. package/runtime/shared/questions/engine.ts +209 -0
  217. package/runtime/shared/questions/index.ts +39 -0
  218. package/runtime/shared/questions/messages/de.json +26 -0
  219. package/runtime/shared/questions/messages/en.json +26 -0
  220. package/runtime/shared/questions/question.ts +430 -0
  221. package/runtime/shared/questions/question.v1.schema.json +342 -0
  222. package/runtime/shared/questions/questions.css +192 -0
  223. package/runtime/shared/questions/render.ts +247 -0
  224. package/runtime/shared/questions/validate.ts +518 -0
  225. package/runtime/shared/schema.ts +44 -0
  226. package/runtime/shared/text/check.ts +68 -0
  227. package/runtime/shared/text/formula.ts +69 -0
  228. package/runtime/shared/text/markdown.ts +185 -0
  229. package/runtime/shared/text/text.v1.schema.json +34 -0
  230. package/runtime/shared/vue-code.ts +395 -0
  231. package/runtime/shared/vue.ts +38 -0
@@ -0,0 +1,446 @@
1
+ // What the build writes for a self-assessment (assessment.ts): the offer on the start page or the chapter's page, and
2
+ // every part of its page but a line for a reader without JavaScript, each in a <template>, inert until the client
3
+ // module puts it into the page: without JavaScript the page shows no heading, button or exercise of it; and its tutor page, <assessment>/tutor/ with its Markdown beside it (index.md), the
4
+ // one source of what a tutor gets about a run. The page holds the intro, each of its pages with its exercises (a
5
+ // question of the question types rendered as the shared questions render it, without "Prüfen"; a generated one as the
6
+ // place of its live task, which the client module draws), the place of the result with the way to the tutor, and the
7
+ // data the client module needs (assessment-client.ts). The tutor page makes the tutor a guide: chapter by chapter to
8
+ // the pages that do not stick (opened with via=tutor, each ends in "Zurück zum Tutor"), then into a new round of the
9
+ // chapter's self-assessment, whose "Zurück zum Tutor" copies its result; after each round the tutor decides on the next
10
+ // chapter or another round. It holds how to read a result (?a1=17-falsch-F1), every chapter with its pages and the
11
+ // link to its self-assessment, and the exercise behind each key; explaining is left to the pages' own tutor pages.
12
+ import { renderQuestion } from "../shared/questions/render.ts";
13
+ import { type MaterialContext, renderMaterial } from "../shared/blocks/index.ts";
14
+ import { plain, renderWithFormulas } from "../shared/text/markdown.ts";
15
+ import {
16
+ assessments,
17
+ drawsTasks,
18
+ type Assessment,
19
+ type AssessmentItem,
20
+ type LinkChapter,
21
+ titleOf,
22
+ } from "./assessment.ts";
23
+ import { inLanguage, type BlockOf, type Site, type Text } from "./content.ts";
24
+ import { exerciseData, type ExerciseData } from "./exercises.ts";
25
+ import { message } from "./messages.ts";
26
+ import { PROVIDERS, placeholderPrompt } from "./prompt.ts";
27
+ import { TUTOR_PAGE } from "./tutor.ts";
28
+ import { needsLines, TUTOR_MARKDOWN, tutorDocument } from "./tutorpage.ts";
29
+
30
+ /** What rendering a part of a self-assessment needs of the page it is on (render.ts). */
31
+ export interface AssessmentContext {
32
+ site: Site;
33
+ lang: string;
34
+ /** A link from this page to `local`, an address within the language tree. */
35
+ to: (local: string) => string;
36
+ /** Where a link inside a text leads. */
37
+ resolve: (href: string) => string;
38
+ material: MaterialContext;
39
+ /** The app's assistants as the options of a select, and copying the text. */
40
+ ways: string;
41
+ /** An exercise component as the exercise `key` of the self-assessment, with its task numbers' seeds. */
42
+ component: (block: BlockOf<"vue-component">, key: string) => { html: string; seeds: number[] } | undefined;
43
+ }
44
+
45
+ /**
46
+ * What the browser reads of one exercise of a self-assessment: a question or a generated exercise as on its page, or
47
+ * an exercise component, whose task the islands show (islands.ts), with the seed of each task number.
48
+ */
49
+ export type AssessmentItemData = ExerciseData | { kind: "component"; seeds: number[] };
50
+
51
+ /** The address of a self-assessment's tutor page within its language tree. */
52
+ export const assessmentTutorOf = (assessment: Pick<Assessment, "local">): string => `${assessment.local}${TUTOR_PAGE}/`;
53
+
54
+ /** What the client module reads from a self-assessment's page (script.explainer-assessment-data). */
55
+ export interface AssessmentData {
56
+ /** The key of its last result on the device, and the app's name for what it keeps there. */
57
+ key: string;
58
+ storage: string;
59
+ steps: number;
60
+ items: {
61
+ key: string;
62
+ ref: string;
63
+ /** Its content page, <chapter>/<page>/. */
64
+ page: string;
65
+ exercise: string;
66
+ step: number;
67
+ data: AssessmentItemData;
68
+ }[];
69
+ /** The content pages it says something about, in order. */
70
+ pages: { page: string; title: string; href: string }[];
71
+ /** Every chapter of the app with its pages and its self-assessment, for the links a tutor gets (assessment.ts). */
72
+ chapters: LinkChapter[];
73
+ /** The address of the app's self-assessment, within the language tree. */
74
+ whole: string;
75
+ /** The root of the language tree, from this page; the tutor page and its Markdown from that root. */
76
+ root: string;
77
+ tutor: string;
78
+ markdown: string;
79
+ ways: { name: string; url: string; paste?: boolean }[];
80
+ placeholder: string;
81
+ }
82
+
83
+ const json = (value: unknown) => JSON.stringify(value).replace(/</g, "\\u003c");
84
+
85
+ /**
86
+ * The offer of a self-assessment on the start page or its chapter's page, in a <template> until the client module
87
+ * puts it into the page; it says "Dein letztes Ergebnis ansehen" when the device keeps one.
88
+ */
89
+ export function assessmentOffer(context: AssessmentContext, assessment: Assessment): string {
90
+ const { lang, site } = context;
91
+ const t = (key: Parameters<typeof message>[1], params?: Record<string, string>) => plain(message(lang, key, params));
92
+ const offer = assessment.chapter === undefined ? "assessmentOfferApp" : "assessmentOfferChapter";
93
+ return [
94
+ `<template class="explainer-assessment-template">`,
95
+ `<section class="explainer-assessment-offer" aria-labelledby="selbsteinschaetzung" hidden data-key="${plain(assessment.key)}" data-storage="${plain(site.storage ?? "explainer")}">`,
96
+ `<h2 id="selbsteinschaetzung">${t("assessmentOfferTitle")}</h2>`,
97
+ `<p>${t(offer, { count: String(assessment.items.length) })}</p>`,
98
+ `<p><a class="explainer-assessment-link" href="${context.to(assessment.local)}" data-last="${t("assessmentOfferLast")}">${t("assessmentOfferStart")}</a></p>`,
99
+ `</section>`,
100
+ `</template>`,
101
+ ].join("\n");
102
+ }
103
+
104
+ /**
105
+ * One exercise of a self-assessment: its number, and its question, the place of its live task, or the component
106
+ * (`component`, its markup).
107
+ */
108
+ function itemHtml(context: AssessmentContext, data: AssessmentItemData, n: number, component = ""): string {
109
+ const { lang } = context;
110
+ const label = `<p class="explainer-assessment-label">${plain(message(lang, "assessmentTask", { n: String(n) }))}</p>`;
111
+ if (data.kind === "generated") return `${label}<div class="explainer-assessment-live"></div>`;
112
+ if (data.kind === "component") return `${label}${component}`;
113
+ // an open task: its task, and the place its answer and ticks go (open-task.ts); no points, no model answer
114
+ if (data.kind === "open")
115
+ return `${label}<div class="explainer-open-task-text">${data.task}</div><div class="explainer-open-live" hidden></div>`;
116
+ const { question, rendered } = data;
117
+ return `${label}${renderQuestion(question, {
118
+ lang,
119
+ format: (text) => rendered[text] ?? renderWithFormulas(text),
120
+ ...(question.material === undefined ? {} : { materialHtml: renderMaterial(question.material, context.material) }),
121
+ })}`;
122
+ }
123
+
124
+ /**
125
+ * The page of a self-assessment: the line for a reader without JavaScript, and, in a <template> until the client
126
+ * 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
127
+ * result with the way to the tutor, and the client module's data.
128
+ */
129
+ export function assessmentMain(context: AssessmentContext, assessment: Assessment): string {
130
+ const { lang, site } = context;
131
+ const t = (key: Parameters<typeof message>[1], params?: Record<string, string>) => plain(message(lang, key, params));
132
+ const items = assessment.items.map((item): { item: AssessmentItem; data: AssessmentItemData; html?: string } => {
133
+ if (item.block.type === "vue-component") {
134
+ const component = context.component(item.block, item.key);
135
+ return { item, data: { kind: "component", seeds: component?.seeds ?? [1] }, html: component?.html ?? "" };
136
+ }
137
+ const data = exerciseData(item.block, lang, site.data, context.resolve);
138
+ // the question by its key: the same id may be on two pages
139
+ return { item, data: data.kind === "question" ? { ...data, question: { ...data.question, id: item.key } } : data };
140
+ });
141
+ const steps = Array.from({ length: assessment.steps }, (_step, index) => {
142
+ const own = items.filter(({ item }) => item.step === index);
143
+ const last = index === assessment.steps - 1;
144
+ return [
145
+ `<section class="explainer-assessment-step" data-step="${index}" aria-labelledby="teil-${index + 1}" hidden>`,
146
+ `<h2 id="teil-${index + 1}" tabindex="-1">${t("assessmentStep", { n: String(index + 1), count: String(assessment.steps) })}</h2>`,
147
+ `<ol class="explainer-assessment-items">`,
148
+ ...own.map(
149
+ ({ item, data, html }) =>
150
+ `<li class="explainer-assessment-item" data-key="${item.key}">${itemHtml(context, data, items.findIndex((other) => other.item === item) + 1, html)}</li>`,
151
+ ),
152
+ `</ol>`,
153
+ `<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>`,
154
+ `</section>`,
155
+ ].join("\n");
156
+ });
157
+ const tutor = assessmentTutorOf(assessment);
158
+ const all = assessments(site);
159
+ const whole = all.find((candidate) => candidate.chapter === undefined);
160
+ const data: AssessmentData = {
161
+ key: assessment.key,
162
+ storage: site.storage ?? "explainer",
163
+ steps: assessment.steps,
164
+ items: items.map(({ item, data: exercise }) => ({
165
+ key: item.key,
166
+ ref: item.ref,
167
+ page: `${item.page.chapter}/${item.page.id}/`,
168
+ exercise: item.exercise,
169
+ step: item.step,
170
+ data: exercise,
171
+ })),
172
+ pages: assessment.pages.map((page) => ({
173
+ page: `${page.chapter}/${page.id}/`,
174
+ title: titleOf(page, lang),
175
+ href: context.to(`${page.chapter}/${page.id}/`),
176
+ })),
177
+ chapters: site.chapters.map((chapter) => {
178
+ const own =
179
+ all.find((candidate) => candidate.chapter === chapter) ?? (site.chapters.length === 1 ? whole : undefined);
180
+ return {
181
+ title: inLanguage(chapter.title, lang).replace(/\s+/g, " ").trim(),
182
+ ...(own === undefined ? {} : { assessment: own.local }),
183
+ pages: chapter.pages.map((page) => ({ title: titleOf(page, lang), local: `${page.chapter}/${page.id}/` })),
184
+ };
185
+ }),
186
+ whole: whole?.local ?? assessment.local,
187
+ root: context.to(""),
188
+ tutor,
189
+ markdown: `${tutor}${TUTOR_MARKDOWN}`,
190
+ ways: PROVIDERS.map((provider) => ({
191
+ name: provider.name,
192
+ url: provider.url,
193
+ ...(provider.paste === true ? { paste: true } : {}),
194
+ })),
195
+ placeholder: placeholderPrompt(lang),
196
+ };
197
+ const back = assessment.chapter === undefined ? "" : `${assessment.chapter.id}/`;
198
+ // what it covers and what not: the content pages none of its exercises asks, each a link to look at
199
+ const unasked = assessment.pages.filter((page) => !assessment.items.some((item) => item.page === page));
200
+ const unaskedText =
201
+ unasked.length === 0
202
+ ? t("assessmentAllAsked")
203
+ : t(unasked.length === 1 ? "assessmentLeftOutOne" : "assessmentLeftOut", { n: String(unasked.length) });
204
+ return [
205
+ `<h1>${t("assessmentTitle")}</h1>`,
206
+ `<p class="explainer-assessment-nojs">${t("assessmentNojs")} <a href="${context.to(back)}">${plain(assessment.chapter === undefined ? message(lang, "start") : inLanguage(assessment.chapter.title, lang))}</a></p>`,
207
+ `<template class="explainer-assessment-template">`,
208
+ `<div class="explainer-assessment" hidden>`,
209
+ `<div class="explainer-assessment-intro">`,
210
+ `<p>${t("assessmentIntro", { count: String(assessment.items.length) })} ${unaskedText}</p>`,
211
+ ...(unasked.length === 0
212
+ ? []
213
+ : [
214
+ `<ul class="explainer-assessment-unasked">`,
215
+ ...unasked.map(
216
+ (page) =>
217
+ `<li><a href="${context.to(`${page.chapter}/${page.id}/`)}">${plain(titleOf(page, lang))}</a></li>`,
218
+ ),
219
+ `</ul>`,
220
+ ]),
221
+ `<p>${t("assessmentHow")}</p>`,
222
+ `<p class="explainer-assessment-privacy">${t("assessmentPrivacy")}</p>`,
223
+ `<p><button type="button" class="explainer-assessment-begin">${t("assessmentBegin")}</button></p>`,
224
+ `</div>`,
225
+ ...steps,
226
+ `<section class="explainer-assessment-result" aria-labelledby="ergebnis" hidden>`,
227
+ `<h2 id="ergebnis" tabindex="-1">${t("assessmentResultTitle")}</h2>`,
228
+ `<p class="explainer-assessment-score"></p>`,
229
+ `<p class="explainer-assessment-advice"></p>`,
230
+ `<ol class="explainer-assessment-pages"></ol>`,
231
+ `<div class="explainer-assessment-tutor">`,
232
+ `<h3>${t("assessmentTutorTitle")}</h3>`,
233
+ `<p>${t("assessmentTutorIntro")}</p>`,
234
+ `<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>`,
235
+ `<p class="explainer-tutor-hint" role="status" aria-live="polite" hidden></p>`,
236
+ `<p class="explainer-assessment-tutor-status" role="status" aria-live="polite"></p>`,
237
+ `<p class="explainer-tutor-note">${t("tutorAccount")}</p>`,
238
+ `<p><a class="explainer-assessment-tutor-page" href="${context.to(tutor)}">${t("assessmentTutorPage")}</a></p>`,
239
+ `</div>`,
240
+ `<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>`,
241
+ `<p class="explainer-assessment-status" role="status" aria-live="polite"></p>`,
242
+ `</section>`,
243
+ `<script type="application/json" class="explainer-assessment-data">${json(data)}</script>`,
244
+ `</div>`,
245
+ `</template>`,
246
+ ].join("\n");
247
+ }
248
+
249
+ interface Words {
250
+ title: string;
251
+ description: string;
252
+ intro: string;
253
+ scopeApp: string;
254
+ scopeChapter: string;
255
+ parents: string;
256
+ toAssessment: string;
257
+ readTitle: string;
258
+ read: string[];
259
+ guideTitle: string;
260
+ guide: string[];
261
+ appRules: string;
262
+ chaptersTitle: string;
263
+ chapter: string;
264
+ assessmentOf: string;
265
+ noAssessment: string;
266
+ whole: string;
267
+ page: string;
268
+ tutorPage: string;
269
+ asked: string;
270
+ unasked: string;
271
+ itemsTitle: string;
272
+ item: string;
273
+ /** After an exercise whose task is the same in every round and on its page. */
274
+ same: string;
275
+ /** A title in quotes. */
276
+ quoted: string;
277
+ }
278
+
279
+ const WORDS: Readonly<Record<string, Words>> = {
280
+ de: {
281
+ title: "Selbsteinschätzung – für deinen KI-Tutor",
282
+ description: "Was ein KI-Tutor zur Selbsteinschätzung von „{app}“ bekommt.",
283
+ intro:
284
+ "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.",
285
+ scopeApp: "der ganzen App",
286
+ scopeChapter: "des Kapitels „{chapter}“",
287
+ parents: "Eltern und Lehrkräfte sehen hier, was der Tutor bekommt.",
288
+ toAssessment: "Zur Selbsteinschätzung",
289
+ readTitle: "So liest du ein Ergebnis",
290
+ read: [
291
+ "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.",
292
+ "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.",
293
+ "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.",
294
+ "„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.",
295
+ ],
296
+ guideTitle: "So leitest du an",
297
+ guide: [
298
+ "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.",
299
+ "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).",
300
+ "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.",
301
+ "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.“",
302
+ "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.",
303
+ "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.",
304
+ "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.",
305
+ "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.",
306
+ "Die Aufgaben der Selbsteinschätzung erklärst du nicht und verrätst keine Lösung: Ihre Seiten haben dieselben Übungen, dort übt das Kind.",
307
+ "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.",
308
+ "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.",
309
+ ],
310
+ appRules: "Regeln dieser App",
311
+ chaptersTitle: "Die Kapitel und ihre Seiten, in ihrer Reihenfolge",
312
+ chapter: "Kapitel",
313
+ assessmentOf: "Selbsteinschätzung des Kapitels",
314
+ noAssessment: "keine Selbsteinschätzung",
315
+ whole: "Selbsteinschätzung der ganzen App",
316
+ page: "Seite",
317
+ tutorPage: "Tutor-Seite",
318
+ asked: "Aufgaben",
319
+ unasked: "nicht abgefragt",
320
+ itemsTitle: "Die Aufgaben dieser Selbsteinschätzung",
321
+ item: "{key}: Übung {exercise} der Seite „{page}“",
322
+ same: " (immer dieselbe Aufgabe, auch auf der Seite: richtig kann gemerkt sein)",
323
+ quoted: "„{title}“",
324
+ },
325
+ en: {
326
+ title: "Self-assessment – for your AI tutor",
327
+ description: "What an AI tutor gets for the self-assessment of “{app}”.",
328
+ intro:
329
+ "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.",
330
+ scopeApp: "of the whole app",
331
+ scopeChapter: "of the chapter “{chapter}”",
332
+ parents: "Parents and teachers see here what the tutor gets.",
333
+ toAssessment: "To the self-assessment",
334
+ readTitle: "How you read a result",
335
+ read: [
336
+ "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.",
337
+ "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.",
338
+ "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.",
339
+ "“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.",
340
+ ],
341
+ guideTitle: "How you guide",
342
+ guide: [
343
+ "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.",
344
+ "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).",
345
+ "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.",
346
+ "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.”",
347
+ "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.",
348
+ "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.",
349
+ "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.",
350
+ "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.",
351
+ "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.",
352
+ "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.",
353
+ "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.",
354
+ ],
355
+ appRules: "Rules of this app",
356
+ chaptersTitle: "The chapters and their pages, in their order",
357
+ chapter: "Chapter",
358
+ assessmentOf: "self-assessment of the chapter",
359
+ noAssessment: "no self-assessment",
360
+ whole: "Self-assessment of the whole app",
361
+ page: "Page",
362
+ tutorPage: "Tutor page",
363
+ asked: "Tasks",
364
+ unasked: "not asked",
365
+ itemsTitle: "The tasks of this self-assessment",
366
+ item: "{key}: exercise {exercise} of the page “{page}”",
367
+ same: " (always the same task, on its page too: right may be remembered)",
368
+ quoted: "“{title}”",
369
+ },
370
+ };
371
+
372
+ const wordsIn = (lang: string): Words => WORDS[lang] ?? (WORDS["de"] as Words);
373
+
374
+ /**
375
+ * The tutor page of a self-assessment in `lang`, as Markdown, its links relative to its folder <assessment>/tutor/: the
376
+ * guide of a tutor who leads the learner chapter by chapter to the pages that do not stick and into a new round of
377
+ * the chapter's self-assessment, how to read a result, every chapter of the app with its pages (the exercises of this
378
+ * self-assessment by key, or not asked) and the link to its self-assessment, and the exercise behind each key.
379
+ */
380
+ export function assessmentTutorMarkdown(site: Site, assessment: Assessment, lang: string): string {
381
+ const words = wordsIn(lang);
382
+ const own = (text: Text) => inLanguage(text, lang).replace(/\s+/g, " ").trim();
383
+ const app = own(site.app.title);
384
+ const scope =
385
+ assessment.chapter === undefined
386
+ ? words.scopeApp
387
+ : words.scopeChapter.replace("{chapter}", own(assessment.chapter.title));
388
+ const here = assessmentTutorOf(assessment);
389
+ const all = assessments(site);
390
+ const whole = all.find((candidate) => candidate.chapter === undefined);
391
+ const lines = [
392
+ `# ${words.title}`,
393
+ "",
394
+ words.intro.replace("{app}", app).replace("{scope}", scope),
395
+ "",
396
+ `${words.parents} [${words.toAssessment}](../)`,
397
+ "",
398
+ `## ${words.readTitle}`,
399
+ "",
400
+ ...words.read.map((text) => `- ${text}`),
401
+ "",
402
+ `## ${words.guideTitle}`,
403
+ "",
404
+ ...words.guide.map((text) => `- ${text.replace("{here}", here)}`),
405
+ "",
406
+ ];
407
+ if (site.app.tutor?.rules !== undefined) {
408
+ lines.push(`## ${words.appRules}`, "", inLanguage(site.app.tutor.rules, lang).trim(), "");
409
+ }
410
+ lines.push(...needsLines(lang), `## ${words.chaptersTitle}`, "");
411
+ for (const chapter of site.chapters) {
412
+ // a chapter's own self-assessment; in an app with one chapter, the app's is the chapter's
413
+ const its =
414
+ all.find((candidate) => candidate.chapter === chapter) ?? (site.chapters.length === 1 ? whole : undefined);
415
+ lines.push(
416
+ `### ${words.chapter} ${words.quoted.replace("{title}", own(chapter.title))}`,
417
+ "",
418
+ `- ${its === undefined ? words.noAssessment : words.assessmentOf}`,
419
+ );
420
+ for (const page of chapter.pages) {
421
+ const keys = assessment.items.filter((item) => item.page === page).map((item) => item.key.toUpperCase());
422
+ const asked = assessment.pages.includes(page)
423
+ ? ` · ${keys.length === 0 ? words.unasked : `${words.asked} ${keys.join(", ")}`}`
424
+ : "";
425
+ lines.push(`- ${words.quoted.replace("{title}", titleOf(page, lang))}${asked}`);
426
+ }
427
+ lines.push("");
428
+ }
429
+ lines.push(`## ${words.itemsTitle}`, "");
430
+ for (const item of assessment.items) {
431
+ lines.push(
432
+ `- ${words.item
433
+ .replace("{key}", item.key.toUpperCase())
434
+ .replace("{exercise}", item.exercise)
435
+ .replace("{page}", titleOf(item.page, lang))}${drawsTasks(item.block) ? "" : words.same}`,
436
+ );
437
+ }
438
+ return `${lines.join("\n").trimEnd()}\n`;
439
+ }
440
+
441
+ /** The tutor page of a self-assessment in `lang` as a whole HTML document. */
442
+ export function assessmentTutorDocument(markdown: string, site: Site, lang: string): string {
443
+ const words = wordsIn(lang);
444
+ const app = inLanguage(site.app.title, lang).replace(/\s+/g, " ").trim();
445
+ return tutorDocument(markdown, `${words.title} – ${app}`, words.description.replace("{app}", app), lang);
446
+ }