@lernapps/app-templates 0.0.0-stage → 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 +144 -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,478 @@
1
+ // The self-assessment in the browser (assessment.ts), started by the client module (explainer.ts). It puts into the
2
+ // page what the build wrote into a <template> for it. On the start page or a chapter's page it shows the offer, and names the last result when the device keeps one.
3
+ // On the self-assessment's page it shows the intro, or the last result when the device keeps one. A start draws a new
4
+ // task of each generated exercise and exercise component (a random task number; the islands show a component's task,
5
+ // islands.ts) and shows the pages one by one, "Weiter" and "Zurück"
6
+ // between them: the exercises have no "Prüfen" and give no feedback. "Auswerten" checks them all together, keeps the
7
+ // result on the device (it replaces the one before) and shows it: for each content page whether the learner probably
8
+ // knows it, should look at it again or should read it, with its link. The way to the tutor takes the result along on
9
+ // a click: the self-assessment's tutor page with the result in its address, and a line per exercise (the input of a
10
+ // wrong one), in the address of the assistant chosen, or copied with the tutor page's Markdown. Nothing goes over the
11
+ // network but that click.
12
+ import { moveFocus } from "@lernapps/tooling/a11y";
13
+ import { createStorage } from "@lernapps/tooling/storage";
14
+ import { startOpenTask, type OpenTaskResult } from "./open-task.ts";
15
+ import { withLanguage, withNeeds, type NeedsChoice } from "./needs.ts";
16
+ import { readAnswer } from "../shared/questions/engine.ts";
17
+ import { renderQuestion } from "../shared/questions/render.ts";
18
+ import { isComplete, isCorrect, shuffled, type Question } from "../shared/questions/question.ts";
19
+ import {
20
+ assessmentPrompt,
21
+ BAND_MESSAGES,
22
+ linkLines,
23
+ isStoredResult,
24
+ pageResults,
25
+ resultQuery,
26
+ type Band,
27
+ type ItemResult,
28
+ } from "./assessment.ts";
29
+ import type { AssessmentData } from "./assessment-page.ts";
30
+ import { mistakeOf, questionFor } from "./generated/question.ts";
31
+ import { taskText } from "./generated/texts.ts";
32
+ import { generate } from "./generated/values.ts";
33
+ import { message, type Messages } from "./messages.ts";
34
+ import { answerShown } from "./result.ts";
35
+ import {
36
+ ASSESSMENT_ANSWER_EVENT,
37
+ ASSESSMENT_TASK_EVENT,
38
+ ASSESSMENT_UNPACKED_EVENT,
39
+ randomTaskNumber,
40
+ type AssessmentAnswer,
41
+ } from "./tasks.ts";
42
+
43
+ /** A way to the tutor: an assistant of the app, or copying the text (no address). */
44
+ interface Way {
45
+ name: string;
46
+ url?: string;
47
+ paste?: boolean;
48
+ }
49
+
50
+ /** What the self-assessment uses of the client module's way to the tutor (explainer.ts). */
51
+ export interface TutorWays {
52
+ /** The way the learner switched on, if any, kept under the app's name `storage`, of the assistants `ways`. */
53
+ chosen: (storage: string, ways: readonly Way[]) => Way | undefined;
54
+ /** Opens the assistant `way` with the prompt, or copies it; `say` tells whether copying worked. */
55
+ open: (
56
+ way: Way | undefined,
57
+ prompts: { start: () => string; copy: () => string | Promise<string>; placeholder: string },
58
+ say: (copied: boolean | undefined) => void,
59
+ ) => void;
60
+ /** Shows in `hint` how the chosen option works, when it says so. */
61
+ hint: (select: HTMLSelectElement, hint: HTMLElement | null) => void;
62
+ /** The needs the learner chose for the tutor, kept under the app's name `storage` or for every explainer. */
63
+ needs: (storage: string) => NeedsChoice | undefined;
64
+ /** Whether a tutor opened the page (via=tutor): its chat runs already. */
65
+ running: boolean;
66
+ /**
67
+ * Copies `text` for the running chat: `button` says it is copied and where to paste it (the assistant switched on
68
+ * under the app's name `storage`, of `ways`), and `place` says it too.
69
+ */
70
+ back: (text: string, button: HTMLButtonElement, place: HTMLElement, storage: string, ways: readonly Way[]) => void;
71
+ }
72
+
73
+ /** The value of copying the text in the select of the ways. */
74
+ const COPY = "copy";
75
+
76
+ /** The words of a band in the result. */
77
+ const BAND: Readonly<Record<Band, keyof Messages>> = BAND_MESSAGES;
78
+
79
+ /** The offer on the start page or a chapter's page: shown, and naming the last result when the device keeps one. */
80
+ function startOffers(root: Document): void {
81
+ for (const offer of root.querySelectorAll<HTMLElement>(".explainer-assessment-offer")) {
82
+ const storage = createStorage(offer.dataset["storage"] ?? "explainer");
83
+ const link = offer.querySelector<HTMLAnchorElement>(".explainer-assessment-link");
84
+ const last = link?.dataset["last"];
85
+ if (link && last !== undefined && storage.load(offer.dataset["key"] ?? "", isStoredResult) !== undefined) {
86
+ link.textContent = last;
87
+ }
88
+ offer.hidden = false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * The options of a choice in an order of its own, new for each run, so that a second run asks the question, not the
94
+ * place of its answer.
95
+ */
96
+ function mix(place: HTMLElement, question: Question): void {
97
+ if (question.type !== "single-choice" && question.type !== "multiple-choice") return;
98
+ const options = [...place.querySelectorAll<HTMLElement>(".question-option")];
99
+ const parent = options[0]?.parentElement;
100
+ if (!parent) return;
101
+ for (const index of shuffled(options.length, Math.floor(Math.random() * 2 ** 31))) {
102
+ const option = options[index];
103
+ if (option) parent.append(option);
104
+ }
105
+ }
106
+
107
+ /** A task number of 1 to `count`, at random, avoiding the numbers in `avoid` while others are left. */
108
+ function drawNumber(count: number, avoid: readonly ReadonlySet<number>[]): number {
109
+ const left = Array.from({ length: count }, (_n, index) => index + 1).filter((n) => avoid.every((set) => !set.has(n)));
110
+ return left.length === 0 ? randomTaskNumber(count) : (left[Math.floor(Math.random() * left.length)] ?? 1);
111
+ }
112
+
113
+ /** The task numbers of the round before, by exercise (its ref), from the address's vorher=a1.28,a4.2. */
114
+ function previousNumbers(href: string, items: readonly { key: string; ref: string }[]): Map<string, Set<number>> {
115
+ const numbers = new Map<string, Set<number>>();
116
+ for (const pair of (new URL(href).searchParams.get("vorher") ?? "").split(",")) {
117
+ const [key, n] = pair.split(".");
118
+ const ref = items.find((item) => item.key === key)?.ref;
119
+ if (ref === undefined || !Number.isInteger(Number(n))) continue;
120
+ numbers.set(ref, (numbers.get(ref) ?? new Set<number>()).add(Number(n)));
121
+ }
122
+ return numbers;
123
+ }
124
+
125
+ /**
126
+ * One exercise of a run: how to read and check its answer; of an exercise component, the islands judge it
127
+ * (`component`).
128
+ */
129
+ interface Live {
130
+ question?: Question;
131
+ component?: true;
132
+ /** What became of an open task so far. */
133
+ open?: () => OpenTaskResult;
134
+ /** The task number of a generated exercise or an exercise component. */
135
+ number?: number;
136
+ /** The typical mistake a wrong answer makes, from 0. */
137
+ mistake?: (answer: Parameters<typeof isCorrect>[1]) => number | undefined;
138
+ }
139
+
140
+ /** Puts what the build wrote for JavaScript into the page: each <template> of a self-assessment, in its place. */
141
+ function unpack(root: Document): void {
142
+ for (const template of root.querySelectorAll<HTMLTemplateElement>("template.explainer-assessment-template")) {
143
+ template.replaceWith(root.importNode(template.content, true));
144
+ }
145
+ // the islands hydrate the exercise components in it now
146
+ root.dispatchEvent(new CustomEvent(ASSESSMENT_UNPACKED_EVENT));
147
+ }
148
+
149
+ /** Starts the self-assessment's page, and the offers on the start page and the chapters' pages. */
150
+ export function startAssessment(root: Document, ways: TutorWays): void {
151
+ unpack(root);
152
+ startOffers(root);
153
+ const box = root.querySelector<HTMLElement>(".explainer-assessment");
154
+ const script = box?.querySelector("script.explainer-assessment-data");
155
+ if (!box || !script) return;
156
+ const data = JSON.parse(script.textContent ?? "{}") as AssessmentData;
157
+ const lang = root.documentElement.lang;
158
+ const t = (key: keyof Messages, params?: Readonly<Record<string, string>>) => message(lang, key, params);
159
+ // a run a tutor opened is no self-assessment of the learner's: the device keeps nothing of it
160
+ const storage = ways.running ? undefined : createStorage(data.storage);
161
+ const intro = box.querySelector<HTMLElement>(".explainer-assessment-intro");
162
+ const steps = [...box.querySelectorAll<HTMLElement>(".explainer-assessment-step")];
163
+ const result = box.querySelector<HTMLElement>(".explainer-assessment-result");
164
+ if (!intro || !result) return;
165
+ for (const nojs of root.querySelectorAll<HTMLElement>(".explainer-assessment-nojs")) nojs.hidden = true;
166
+ box.hidden = false;
167
+ // an exercise's form never leaves the page: Enter in a field moves nowhere
168
+ box.addEventListener("submit", (event) => event.preventDefault());
169
+
170
+ const views = [intro, ...steps, result];
171
+ const show = (view: HTMLElement, focus: boolean) => {
172
+ for (const other of views) other.hidden = other !== view;
173
+ const heading = view.querySelector<HTMLElement>("h2");
174
+ if (focus && heading) moveFocus(heading);
175
+ };
176
+
177
+ // the exercises of the run, and the answer each exercise component has now, as the islands judged it
178
+ const live = new Map<string, Live>();
179
+ const answers = new Map<string, AssessmentAnswer>();
180
+ box.addEventListener(ASSESSMENT_ANSWER_EVENT, (event) => {
181
+ const key = (event.target as HTMLElement).closest<HTMLElement>(".explainer-assessment-item")?.dataset["key"];
182
+ const answer = (event as CustomEvent<AssessmentAnswer | null>).detail;
183
+ // none: emptied, incomplete, or one the logic cannot read
184
+ if (key !== undefined) {
185
+ if (answer === null) answers.delete(key);
186
+ else answers.set(key, answer);
187
+ }
188
+ });
189
+ const element = (key: string) => box.querySelector<HTMLElement>(`.explainer-assessment-item[data-key="${key}"]`);
190
+
191
+ // the task numbers of the round before, which a tutor's link names (vorher=a1.28,a4.2): a new round avoids them
192
+ const before = previousNumbers(root.location.href, data.items);
193
+
194
+ /** A new run: a new task of each generated exercise, every answer empty, the options mixed, the first page. */
195
+ const begin = () => {
196
+ live.clear();
197
+ answers.clear();
198
+ // the task numbers drawn of each exercise: listed again, it draws another
199
+ const drawn = new Map<string, Set<number>>();
200
+ for (const item of data.items) {
201
+ const place = element(item.key);
202
+ if (!place) continue;
203
+ const exercise = item.data;
204
+ if (exercise.kind === "question") {
205
+ place.querySelector("form")?.reset();
206
+ mix(place, exercise.question);
207
+ live.set(item.key, { question: exercise.question });
208
+ continue;
209
+ }
210
+ if (exercise.kind === "open") {
211
+ // a new run starts it afresh: its answer and ticks empty
212
+ const { result } = startOpenTask(place, { ...exercise, id: item.key }, { assessment: true });
213
+ live.set(item.key, { open: result });
214
+ continue;
215
+ }
216
+ const { seeds } = exercise;
217
+ const used = drawn.get(item.ref) ?? new Set<number>();
218
+ drawn.set(item.ref, used);
219
+ const number = drawNumber(Math.max(seeds.length, 1), [used, before.get(item.ref) ?? new Set<number>()]);
220
+ used.add(number);
221
+ // the task number drawn, for the tests and a look at the page
222
+ place.dataset["number"] = String(number);
223
+ if (exercise.kind === "component") {
224
+ // the islands show its task: at once when hydrated, else when they hydrate it
225
+ const component = place.querySelector<HTMLElement>(".explainer-component");
226
+ const seed = seeds[number - 1] ?? number;
227
+ if (component) {
228
+ component.dataset["seed"] = String(seed);
229
+ component.dispatchEvent(new CustomEvent(ASSESSMENT_TASK_EVENT, { detail: { seed } }));
230
+ }
231
+ live.set(item.key, { component: true, number });
232
+ continue;
233
+ }
234
+ const { spec, templates } = exercise;
235
+ const task = generate(spec, seeds[number - 1] ?? number, exercise.data);
236
+ const question = { ...questionFor(spec, task, lang, templates), id: item.key };
237
+ const target = place.querySelector<HTMLElement>(".explainer-assessment-live");
238
+ if (target)
239
+ target.innerHTML = renderQuestion(question, { lang, textHtml: taskText(spec, task, lang, templates) });
240
+ mix(place, question);
241
+ live.set(item.key, {
242
+ question,
243
+ number,
244
+ mistake: (answer) => mistakeOf(spec, task, question, answer),
245
+ });
246
+ }
247
+ const first = steps[0];
248
+ if (first) show(first, true);
249
+ };
250
+
251
+ /** What became of each exercise of the run. */
252
+ const evaluate = (): ItemResult[] =>
253
+ data.items.map((item) => {
254
+ const run = live.get(item.key);
255
+ const form = element(item.key)?.querySelector("form");
256
+ const base = { key: item.key, ref: item.ref, ...(run?.number === undefined ? {} : { number: run.number }) };
257
+ if (run?.open !== undefined) {
258
+ const answer = run.open();
259
+ if (answer.state === "offen") return { ...base, state: "offen" };
260
+ return {
261
+ ...base,
262
+ state: answer.state,
263
+ ...(answer.text === undefined || answer.text === "" ? {} : { input: answer.text }),
264
+ ...(answer.missed.length === 0 ? {} : { missed: answer.missed.map((point) => point + 1) }),
265
+ };
266
+ }
267
+ if (run?.component === true) {
268
+ const answer = answers.get(item.key);
269
+ if (answer === undefined) return { ...base, state: "offen" };
270
+ if (answer.correct) return { ...base, state: "richtig" };
271
+ return {
272
+ ...base,
273
+ state: answer.partly === true ? "teilweise" : "falsch",
274
+ ...(answer.answer === undefined ? {} : { input: answer.answer }),
275
+ ...(answer.mistake === undefined || answer.partly === true ? {} : { mistake: answer.mistake + 1 }),
276
+ ...(answer.missed === undefined ? {} : { missed: answer.missed.map((point) => point + 1) }),
277
+ };
278
+ }
279
+ if (!run?.question || !form) return { ...base, state: "offen" };
280
+ const answer = readAnswer(run.question, form);
281
+ if (!isComplete(run.question, answer)) return { ...base, state: "offen" };
282
+ if (isCorrect(run.question, answer)) return { ...base, state: "richtig" };
283
+ const mistake = run.mistake?.(answer);
284
+ return {
285
+ ...base,
286
+ state: "falsch",
287
+ input: answerShown(run.question, answer, lang),
288
+ ...(mistake === undefined ? {} : { mistake: mistake + 1 }),
289
+ };
290
+ });
291
+
292
+ // the result
293
+ const score = result.querySelector<HTMLElement>(".explainer-assessment-score");
294
+ const advice = result.querySelector<HTMLElement>(".explainer-assessment-advice");
295
+ const list = result.querySelector<HTMLElement>(".explainer-assessment-pages");
296
+ const status = result.querySelector<HTMLElement>(".explainer-assessment-status");
297
+ let shown: ItemResult[] = [];
298
+
299
+ /** Shows the result of a run: the score, the advice, and each content page with its band. */
300
+ const showResult = (results: ItemResult[], focus: boolean) => {
301
+ shown = results;
302
+ const pageOf = Object.fromEntries(data.items.map((item) => [item.key, item.page]));
303
+ const pages = pageResults(
304
+ data.pages.map((page) => page.page),
305
+ pageOf,
306
+ results,
307
+ );
308
+ const right = results.filter((one) => one.state === "richtig").length;
309
+ const partly = results.filter((one) => one.state === "teilweise").length;
310
+ if (score)
311
+ score.textContent = t(partly > 0 ? "assessmentScorePartly" : "assessmentScore", {
312
+ right: String(right),
313
+ partly: String(partly),
314
+ total: String(results.length),
315
+ });
316
+ if (advice) {
317
+ const open = pages.some((page) => page.band === "read" || page.band === "partly");
318
+ advice.textContent = t(open ? "assessmentAdvice" : "assessmentAllKnown");
319
+ }
320
+ if (list) {
321
+ list.replaceChildren(
322
+ ...pages.map((page) => {
323
+ const info = data.pages.find((candidate) => candidate.page === page.page);
324
+ const item = root.createElement("li");
325
+ item.className = "explainer-assessment-page";
326
+ item.dataset["band"] = page.band;
327
+ const link = root.createElement("a");
328
+ link.href = info?.href ?? "";
329
+ link.textContent = info?.title ?? page.page;
330
+ const band = root.createElement("strong");
331
+ band.textContent = t(BAND[page.band]);
332
+ item.append(link, " ", band);
333
+ if (page.total > 0) {
334
+ const count = root.createElement("span");
335
+ count.className = "explainer-assessment-count";
336
+ count.textContent = t(page.partly > 0 ? "assessmentPageScorePartly" : "assessmentPageScore", {
337
+ right: String(page.right),
338
+ partly: String(page.partly),
339
+ total: String(page.total),
340
+ });
341
+ item.append(" ", count);
342
+ }
343
+ return item;
344
+ }),
345
+ );
346
+ }
347
+ if (status) status.textContent = "";
348
+ show(result, focus);
349
+ };
350
+
351
+ // the full address of every page and self-assessment, as the browser has them: the tutor builds none itself
352
+ const links = () => linkLines(new URL(data.root, root.location.href).href, data.chapters, data.whole, lang);
353
+
354
+ // what the prompt says of the exercises and of each content page
355
+ const promptItems = () =>
356
+ data.items.map((item) => ({
357
+ key: item.key,
358
+ page: data.pages.find((page) => page.page === item.page)?.title ?? item.page,
359
+ exercise: item.exercise,
360
+ }));
361
+ const promptPages = () =>
362
+ pageResults(
363
+ data.pages.map((page) => page.page),
364
+ Object.fromEntries(data.items.map((item) => [item.key, item.page])),
365
+ shown,
366
+ ).map((page) => ({ ...page, title: data.pages.find((one) => one.page === page.page)?.title ?? page.page }));
367
+
368
+ // the way to the tutor
369
+ const select = result.querySelector<HTMLSelectElement>(".explainer-assessment-way");
370
+ const go = result.querySelector<HTMLButtonElement>(".explainer-assessment-go");
371
+ const said = result.querySelector<HTMLElement>(".explainer-assessment-tutor-status");
372
+ const hint = result.querySelector<HTMLElement>(".explainer-tutor-hint");
373
+ const tutorBox = result.querySelector<HTMLElement>(".explainer-assessment-tutor");
374
+ if (ways.running && tutorBox) {
375
+ // the chat runs: "Zurück zum Tutor" copies the result for it, as every page a tutor opened does
376
+ const back = root.createElement("button");
377
+ back.type = "button";
378
+ back.className = "explainer-assessment-back-to-tutor";
379
+ back.textContent = t("backToTutor");
380
+ const intro = root.createElement("p");
381
+ intro.textContent = t("assessmentBackIntro");
382
+ const row = root.createElement("p");
383
+ row.append(back);
384
+ tutorBox.replaceChildren(intro, row);
385
+ back.addEventListener("click", () => {
386
+ const base = new URL(data.root, root.location.href);
387
+ const tutor = new URL(`${data.tutor}${resultQuery(shown)}`, base).href;
388
+ // the link of the next round: the same self-assessment, with the task numbers of this one to avoid
389
+ const next = new URL(root.location.href);
390
+ next.search = "";
391
+ next.hash = "";
392
+ next.searchParams.set("via", "tutor");
393
+ const numbers = shown.flatMap((one) => (one.number === undefined ? [] : [`${one.key}.${one.number}`]));
394
+ if (numbers.length > 0) next.searchParams.set("vorher", numbers.join(","));
395
+ const lines = assessmentPrompt(tutor, promptItems(), shown, lang, promptPages(), "back", links());
396
+ ways.back(
397
+ `${lines}\n${t("assessmentNextRound", { url: next.href.replace(/%2C/g, ",") })}`,
398
+ back,
399
+ tutorBox,
400
+ data.storage,
401
+ data.ways,
402
+ );
403
+ });
404
+ result.querySelector(".explainer-assessment-delete")?.remove();
405
+ const privacy = root.querySelector<HTMLElement>(".explainer-assessment-privacy");
406
+ if (privacy) privacy.textContent = t("assessmentTutorRun");
407
+ } else if (select && go) {
408
+ const chosen = ways.chosen(data.storage, data.ways);
409
+ if (chosen !== undefined) select.value = chosen.url === undefined ? COPY : chosen.name;
410
+ const way = (): Way | undefined =>
411
+ select.value === COPY ? { name: COPY } : data.ways.find((candidate) => candidate.name === select.value);
412
+ const label = () => {
413
+ const current = way();
414
+ go.textContent = current?.url === undefined ? t("tutorCopy") : t("assessmentTutorGo", { name: current.name });
415
+ ways.hint(select, hint);
416
+ };
417
+ select.addEventListener("change", label);
418
+ label();
419
+ go.addEventListener("click", () => {
420
+ const href = root.location.href;
421
+ const base = new URL(data.root, href);
422
+ // a new chat: with the learner's needs (dec-tutor-needs)
423
+ const needs = ways.needs(data.storage);
424
+ const tutor = new URL(`${data.tutor}${withNeeds(resultQuery(shown), needs)}`, base).href;
425
+ const prompt = () =>
426
+ withLanguage(assessmentPrompt(tutor, promptItems(), shown, lang, promptPages(), "ask", links()), needs, lang);
427
+ const copy = async () => {
428
+ try {
429
+ const response = await fetch(new URL(data.markdown, base).href);
430
+ if (response.ok) return `${prompt()}\n\n${(await response.text()).trimEnd()}\n`;
431
+ } catch {
432
+ // not reachable: the prompt names the tutor page
433
+ }
434
+ return prompt();
435
+ };
436
+ ways.open(way(), { start: prompt, copy, placeholder: data.placeholder }, (copied) => {
437
+ if (said) said.textContent = copied === undefined ? "" : t(copied ? "tutorCopied" : "tutorCopyFailed");
438
+ });
439
+ });
440
+ }
441
+
442
+ // the buttons
443
+ box.querySelector(".explainer-assessment-begin")?.addEventListener("click", begin);
444
+ steps.forEach((step, index) => {
445
+ step.querySelector(".explainer-assessment-back")?.addEventListener("click", () => {
446
+ const before = steps[index - 1];
447
+ if (before) show(before, true);
448
+ });
449
+ step.querySelector(".explainer-assessment-next")?.addEventListener("click", () => {
450
+ const after = steps[index + 1];
451
+ if (after) show(after, true);
452
+ });
453
+ step.querySelector(".explainer-assessment-finish")?.addEventListener("click", () => {
454
+ const results = evaluate();
455
+ storage?.save(data.key, { version: 1, items: results });
456
+ showResult(results, true);
457
+ });
458
+ });
459
+ result.querySelector(".explainer-assessment-restart")?.addEventListener("click", begin);
460
+ result.querySelector(".explainer-assessment-delete")?.addEventListener("click", () => {
461
+ storage?.remove(data.key);
462
+ show(intro, false);
463
+ const button = intro.querySelector<HTMLElement>(".explainer-assessment-begin");
464
+ if (button) button.focus();
465
+ const line = root.createElement("p");
466
+ line.className = "explainer-assessment-deleted";
467
+ line.setAttribute("role", "status");
468
+ line.textContent = t("assessmentDeleted");
469
+ intro.querySelector(".explainer-assessment-deleted")?.remove();
470
+ intro.prepend(line);
471
+ });
472
+
473
+ // the last result, when the device keeps one of exercises the content still has
474
+ const stored = storage?.load(data.key, isStoredResult);
475
+ const kept = stored?.items.filter((one) => data.items.some((item) => item.key === one.key && item.ref === one.ref));
476
+ if (kept !== undefined && kept.length > 0) showResult(kept, false);
477
+ else show(intro, false);
478
+ }