@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.
- package/LICENSE +22 -0
- package/README.md +195 -2
- package/dist/appname-Bh23cUuz.mjs +12 -0
- package/dist/assessment-By2GGTJC.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-BmRrD7UY.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-BkEl-m-F.mjs +254 -0
- package/dist/content-cYBPYg_J.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-BiMI-pCL.mjs +89 -0
- package/dist/engine-BqF2Vffn.mjs +136 -0
- package/dist/explainer-e2e.mjs +1297 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2131 -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-C-0hUaSC.mjs +16947 -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-BKc8JYtC.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-BJey0bwl.mjs +540 -0
- package/dist/render-BL6fV_NI.mjs +507 -0
- package/dist/render-DpRO_RKm.mjs +146 -0
- package/dist/result-_eQcamoz.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-COaZso7u.mjs +1072 -0
- package/dist/validate-CxtdeQ5S.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 +144 -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 +446 -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 +1474 -0
- package/runtime/explainer/exercises.ts +324 -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 +299 -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 +430 -0
- package/runtime/shared/questions/question.v1.schema.json +342 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +247 -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/text.v1.schema.json +34 -0
- package/runtime/shared/vue-code.ts +395 -0
- package/runtime/shared/vue.ts +38 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
// The tutor page of each content page, <chapter>/<page>/tutor/, the one thing a KI tutor fetches and the one source
|
|
2
|
+
// of what it gets. A chat assistant fetches only addresses its user wrote into the conversation, not those it finds in
|
|
3
|
+
// a fetched file, and a static host serves the same file for any query: so the page holds every task of every
|
|
4
|
+
// exercise, and the address the learner's message names says where the learner is, the task number of each exercise
|
|
5
|
+
// as in the app's own address (?<exercise>=<n>), the exercise at hand and the section (position.ts). Two ways lead the
|
|
6
|
+
// learner to the tutor: the bar beside a page's title (way A, prompt.ts) and "Hilfe" at one task (way B, stuck.ts),
|
|
7
|
+
// both naming this page; the result line of "Zurück" names it again (result.ts).
|
|
8
|
+
//
|
|
9
|
+
// The tutor page is short, lean HTML without script or menu: parents and teachers open the same address and see what
|
|
10
|
+
// the tutor gets, and every word costs the tutor tokens and makes its answers longer. Its guide is visible, never
|
|
11
|
+
// hidden, as hidden instructions to an AI look like prompt injection: who the tutor helps and where the child is; how
|
|
12
|
+
// to guide (short answers, ask what is not understood, step by step from the topic to the task, the page's own tasks,
|
|
13
|
+
// nothing worked out by itself, no solution, practising in the app, never copying a task); how to read where the
|
|
14
|
+
// child is and how a task's link looks; the app's own rules; the learner's needs, a short section for each, which holds
|
|
15
|
+
// when the query names it in `bedarf=` (needs.ts). Then the page's content as context (tutor.ts,
|
|
16
|
+
// contentLines), and its exercises: a generated one once as its template, with hint, steps and its typical mistakes
|
|
17
|
+
// as F1, F2 …, and below a table of each task number's inputs and outputs, the values of its placeholders, the
|
|
18
|
+
// solution and what each mistake gives, all worked out; an exercise component by its about in prose, and with its
|
|
19
|
+
// table of tasks (template, params, mistakes and show of its logic) its task once as a template and the same table as
|
|
20
|
+
// a generated one, else each number's task and solution from its describe, as many numbers as fit into
|
|
21
|
+
// TUTOR_PAGE_BUDGET; a question without numbers in full. The build writes the page's Markdown beside it (index.md) for
|
|
22
|
+
// an assistant that fetches nothing, checks each of its links (tutor.ts, linkProblems) and notes a page beyond the
|
|
23
|
+
// budget or an exercise with numbers left out.
|
|
24
|
+
import { Marked } from "marked";
|
|
25
|
+
import { TABLE_CONTRACT } from "./components/exercise.ts";
|
|
26
|
+
import { escape } from "../shared/html.ts";
|
|
27
|
+
import { answerText } from "../shared/questions/render.ts";
|
|
28
|
+
import {
|
|
29
|
+
inLanguage,
|
|
30
|
+
type Block,
|
|
31
|
+
type ComponentExercise,
|
|
32
|
+
type Descriptions,
|
|
33
|
+
type Page,
|
|
34
|
+
type Site,
|
|
35
|
+
type Text,
|
|
36
|
+
} from "./content.ts";
|
|
37
|
+
import { dataOf, taskSeeds, templatesOf } from "./exercises.ts";
|
|
38
|
+
import { questionFor } from "./generated/question.ts";
|
|
39
|
+
import { label, placeholderValue, placeholdersIn, plainText } from "./generated/texts.ts";
|
|
40
|
+
import { type Generated } from "./generated/types.ts";
|
|
41
|
+
import { generate } from "./generated/values.ts";
|
|
42
|
+
import { NEEDS, type Need } from "./needs.ts";
|
|
43
|
+
import { contentLines, exercisePart, tutorWords } from "./tutor.ts";
|
|
44
|
+
|
|
45
|
+
/** The name of a tutor page's Markdown, beside the page in its folder: for an assistant that fetches nothing. */
|
|
46
|
+
export const TUTOR_MARKDOWN = "index.md";
|
|
47
|
+
|
|
48
|
+
/** The size a tutor page stays within, in bytes: an exercise component gets as many numbers as fit; the build notes a larger page. */
|
|
49
|
+
export const TUTOR_PAGE_BUDGET = 64_000;
|
|
50
|
+
|
|
51
|
+
/** The size of a text in bytes, as UTF-8. */
|
|
52
|
+
export const bytesOf = (text: string): number => new TextEncoder().encode(text).length;
|
|
53
|
+
|
|
54
|
+
interface Words {
|
|
55
|
+
title: string;
|
|
56
|
+
tutorPage: string;
|
|
57
|
+
forTutor: string;
|
|
58
|
+
intro: string;
|
|
59
|
+
parents: string;
|
|
60
|
+
toPage: string;
|
|
61
|
+
/** The link to the page's scope and what to do when it cannot be fetched. */
|
|
62
|
+
scope: string;
|
|
63
|
+
guideTitle: string;
|
|
64
|
+
guide: string[];
|
|
65
|
+
startTitle: string;
|
|
66
|
+
start: string[];
|
|
67
|
+
appRules: string;
|
|
68
|
+
whereTitle: string;
|
|
69
|
+
where: string[];
|
|
70
|
+
/** The learner's needs (needs.ts): what holds when the query names them, then a short section for each. */
|
|
71
|
+
needsTitle: string;
|
|
72
|
+
needsIntro: string;
|
|
73
|
+
needs: Readonly<Record<Need, string>>;
|
|
74
|
+
contextTitle: string;
|
|
75
|
+
exercise: string;
|
|
76
|
+
steps: string;
|
|
77
|
+
link: string;
|
|
78
|
+
linkNumber: string;
|
|
79
|
+
table: string;
|
|
80
|
+
/** After an input of the table that the task's text does not show. */
|
|
81
|
+
hidden: string;
|
|
82
|
+
answerIs: string;
|
|
83
|
+
number: string;
|
|
84
|
+
choices: string;
|
|
85
|
+
tasks: string;
|
|
86
|
+
shown: string;
|
|
87
|
+
none: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const WORDS: Readonly<Record<string, Words>> = {
|
|
91
|
+
de: {
|
|
92
|
+
title: "{page} – für deinen KI-Tutor",
|
|
93
|
+
tutorPage: "Tutor-Seite",
|
|
94
|
+
forTutor: "Was ein KI-Tutor zur Seite „{page}“ bekommt.",
|
|
95
|
+
intro:
|
|
96
|
+
"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.",
|
|
97
|
+
parents: "Eltern und Lehrkräfte sehen hier, was der Tutor bekommt.",
|
|
98
|
+
toPage: "Zur Seite",
|
|
99
|
+
scope:
|
|
100
|
+
"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.",
|
|
101
|
+
guideTitle: "So leitest du an",
|
|
102
|
+
guide: [
|
|
103
|
+
"Antworte kurz: höchstens drei Sätze und eine Frage. Keine Überschriften, keine Listen.",
|
|
104
|
+
"Nimm die Beispiele und Aufgaben dieser Seite. Erfinde keine eigenen. Rechne nichts selbst aus: Lösungen und Fehler stehen unten bei jeder Nummer.",
|
|
105
|
+
"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.",
|
|
106
|
+
"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.",
|
|
107
|
+
"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.",
|
|
108
|
+
"Bitte das Kind nie, eine Aufgabe abzuschreiben. Du kennst sie aus dem Kontext unten.",
|
|
109
|
+
],
|
|
110
|
+
startTitle: "So beginnst du",
|
|
111
|
+
start: [
|
|
112
|
+
"„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.",
|
|
113
|
+
"„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.",
|
|
114
|
+
],
|
|
115
|
+
appRules: "Regeln dieser App",
|
|
116
|
+
whereTitle: "Wo das Kind gerade ist",
|
|
117
|
+
where: [
|
|
118
|
+
"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.",
|
|
119
|
+
"Steht in der Nachricht „Die App sagt: …“, hat die App das zur Eingabe des Kindes gemeldet. Setz dort an, ohne es zu wiederholen.",
|
|
120
|
+
"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.",
|
|
121
|
+
"„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.",
|
|
122
|
+
"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.“",
|
|
123
|
+
],
|
|
124
|
+
needsTitle: "Besondere Anforderungen",
|
|
125
|
+
needsIntro:
|
|
126
|
+
"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.",
|
|
127
|
+
needs: {
|
|
128
|
+
"kurze-schritte":
|
|
129
|
+
"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.",
|
|
130
|
+
"wenig-lesen":
|
|
131
|
+
"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.",
|
|
132
|
+
"mehr-tiefe":
|
|
133
|
+
"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.",
|
|
134
|
+
"andere-muttersprache":
|
|
135
|
+
"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.",
|
|
136
|
+
"klar-und-woertlich":
|
|
137
|
+
"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“.",
|
|
138
|
+
"rechnen-anschaulich":
|
|
139
|
+
"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.",
|
|
140
|
+
},
|
|
141
|
+
contextTitle: "Kontext: die Seite „{page}“",
|
|
142
|
+
exercise: "Übung",
|
|
143
|
+
steps: "Lösungsweg",
|
|
144
|
+
link: "Link",
|
|
145
|
+
linkNumber: "Link zu Nummer n",
|
|
146
|
+
table:
|
|
147
|
+
"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.",
|
|
148
|
+
hidden: "(verdeckt)",
|
|
149
|
+
answerIs: "`{answer}` ist die Lösung.",
|
|
150
|
+
number: "Nr.",
|
|
151
|
+
choices: "Zur Wahl",
|
|
152
|
+
tasks: "Aufgaben nach Nummer, mit Lösung",
|
|
153
|
+
shown:
|
|
154
|
+
"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.",
|
|
155
|
+
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“.",
|
|
156
|
+
},
|
|
157
|
+
en: {
|
|
158
|
+
title: "{page} – for your AI tutor",
|
|
159
|
+
tutorPage: "tutor page",
|
|
160
|
+
forTutor: "What an AI tutor gets for the page “{page}”.",
|
|
161
|
+
intro:
|
|
162
|
+
"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.",
|
|
163
|
+
parents: "Parents and teachers see here what the tutor gets.",
|
|
164
|
+
toPage: "To the page",
|
|
165
|
+
scope:
|
|
166
|
+
"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.",
|
|
167
|
+
guideTitle: "How you guide",
|
|
168
|
+
guide: [
|
|
169
|
+
"Answer briefly: three sentences and one question at most. No headings, no lists.",
|
|
170
|
+
"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.",
|
|
171
|
+
"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.",
|
|
172
|
+
"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.",
|
|
173
|
+
"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.",
|
|
174
|
+
"Never ask the child to copy a task. You know it from the context below.",
|
|
175
|
+
],
|
|
176
|
+
startTitle: "How you start",
|
|
177
|
+
start: [
|
|
178
|
+
"“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.",
|
|
179
|
+
"“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.",
|
|
180
|
+
],
|
|
181
|
+
appRules: "Rules of this app",
|
|
182
|
+
whereTitle: "Where the child is",
|
|
183
|
+
where: [
|
|
184
|
+
"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.",
|
|
185
|
+
"If the message says “The app says: …”, the app showed that for the child's input. Build on it without repeating it.",
|
|
186
|
+
"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.",
|
|
187
|
+
"“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.",
|
|
188
|
+
"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.”",
|
|
189
|
+
],
|
|
190
|
+
needsTitle: "Special requirements",
|
|
191
|
+
needsIntro:
|
|
192
|
+
"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.",
|
|
193
|
+
needs: {
|
|
194
|
+
"kurze-schritte":
|
|
195
|
+
"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.",
|
|
196
|
+
"wenig-lesen":
|
|
197
|
+
"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.",
|
|
198
|
+
"mehr-tiefe":
|
|
199
|
+
"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.",
|
|
200
|
+
"andere-muttersprache":
|
|
201
|
+
"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.",
|
|
202
|
+
"klar-und-woertlich":
|
|
203
|
+
"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”.",
|
|
204
|
+
"rechnen-anschaulich":
|
|
205
|
+
"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.",
|
|
206
|
+
},
|
|
207
|
+
contextTitle: "Context: the page “{page}”",
|
|
208
|
+
exercise: "Exercise",
|
|
209
|
+
steps: "Steps of the solution",
|
|
210
|
+
link: "Link",
|
|
211
|
+
linkNumber: "Link to number n",
|
|
212
|
+
table:
|
|
213
|
+
"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.",
|
|
214
|
+
hidden: "(hidden)",
|
|
215
|
+
answerIs: "`{answer}` is the solution.",
|
|
216
|
+
number: "No.",
|
|
217
|
+
choices: "Choices",
|
|
218
|
+
tasks: "Tasks by number, with solution",
|
|
219
|
+
shown:
|
|
220
|
+
"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.",
|
|
221
|
+
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”.",
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const wordsIn = (lang: string): Words => WORDS[lang] ?? (WORDS["de"] as Words);
|
|
226
|
+
|
|
227
|
+
/** A text on one line, as a list item holds it, for the page's Markdown (formulas). */
|
|
228
|
+
const line = (text: string) => formulas(text.replace(/\s+/g, " ").trim());
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A text for the page's Markdown, its lines kept: no HTML in it, and each formula $…$ on one line, which the page's
|
|
232
|
+
* Markdown keeps as written (FORMULA).
|
|
233
|
+
*/
|
|
234
|
+
const formulas = (text: string) =>
|
|
235
|
+
text
|
|
236
|
+
.split(/(\$[^$]+\$)/)
|
|
237
|
+
.map((part, index) => (index % 2 === 1 ? part.replace(/\s+/g, " ") : part.replace(/</g, "<")))
|
|
238
|
+
.join("");
|
|
239
|
+
|
|
240
|
+
/** The types of the question blocks, exercises without numbers. */
|
|
241
|
+
const QUESTIONS: ReadonlySet<string> = new Set([
|
|
242
|
+
"single-choice",
|
|
243
|
+
"multiple-choice",
|
|
244
|
+
"true-false",
|
|
245
|
+
"number",
|
|
246
|
+
"ordering",
|
|
247
|
+
"matching",
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
/** An exercise of a page, where it is, and how many numbered tasks it has (0 for one without numbers). */
|
|
251
|
+
export interface PageExercise {
|
|
252
|
+
id: string;
|
|
253
|
+
block: Block;
|
|
254
|
+
/** The anchor of the section it is in (<section>/<subsection>), if any. */
|
|
255
|
+
section?: string;
|
|
256
|
+
sectionTitle?: Text;
|
|
257
|
+
count: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The exercises of `page` in order, each with its section and the number of its numbered tasks. */
|
|
261
|
+
export function pageExercises(site: Site, page: Page, descriptions: Descriptions): PageExercise[] {
|
|
262
|
+
const found: PageExercise[] = [];
|
|
263
|
+
const walk = (blocks: readonly Block[], anchor: string[], title: Text | undefined): void => {
|
|
264
|
+
for (const block of blocks) {
|
|
265
|
+
if (block.type === "section") walk(block.blocks, [...anchor, block.id], block.title);
|
|
266
|
+
else if (block.type === "example" || block.type === "note") walk(block.blocks, anchor, title);
|
|
267
|
+
else {
|
|
268
|
+
const where =
|
|
269
|
+
anchor.length === 0
|
|
270
|
+
? {}
|
|
271
|
+
: { section: anchor.join("/"), ...(title === undefined ? {} : { sectionTitle: title }) };
|
|
272
|
+
if (block.type === "generated") {
|
|
273
|
+
found.push({ id: block.id, block, ...where, count: taskSeeds(block, dataOf(block, site.data)).length });
|
|
274
|
+
} else if (block.type === "vue-component") {
|
|
275
|
+
const use = site.components.find((candidate) => candidate.block === block);
|
|
276
|
+
const description = descriptions.get(block);
|
|
277
|
+
if (use?.id !== undefined && use.component.exercise) {
|
|
278
|
+
found.push({ id: use.id, block, ...where, count: description?.seeds.length ?? 0 });
|
|
279
|
+
}
|
|
280
|
+
} else if (block.type === "open-task") {
|
|
281
|
+
found.push({ id: block.id, block, ...where, count: 0 });
|
|
282
|
+
} else if (QUESTIONS.has(block.type) && "id" in block && typeof block.id === "string") {
|
|
283
|
+
found.push({ id: block.id, block, ...where, count: 0 });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
walk(page.blocks, [], undefined);
|
|
289
|
+
return found;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The anchors of the sections of `page`, nested as <section>/<subsection>. */
|
|
293
|
+
export function sectionsOf(page: Page): string[] {
|
|
294
|
+
const walk = (blocks: readonly Block[], anchor: string[]): string[] =>
|
|
295
|
+
blocks.flatMap((block) =>
|
|
296
|
+
block.type === "section" ? [[...anchor, block.id].join("/"), ...walk(block.blocks, [...anchor, block.id])] : [],
|
|
297
|
+
);
|
|
298
|
+
return walk(page.blocks, []);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** A template of the content with each placeholder {{x}} as {x}, on one line, for the page's Markdown. */
|
|
302
|
+
const template = (text: Text, lang: string) => line(inLanguage(text, lang).replace(/\{\{\s*([\w.]+)\s*\}\}/g, "{$1}"));
|
|
303
|
+
|
|
304
|
+
/** The placeholders of a generated exercise's texts in `lang`, in order and each once, but {{answer}}, the solution. */
|
|
305
|
+
function placeholdersOf(spec: Generated, lang: string): { paths: string[]; answer: boolean } {
|
|
306
|
+
const texts = [
|
|
307
|
+
spec.task.text,
|
|
308
|
+
...(spec.hint === undefined ? [] : [spec.hint]),
|
|
309
|
+
...(spec.steps ?? []),
|
|
310
|
+
...(spec.mistakes ?? []).map((mistake) => mistake.message),
|
|
311
|
+
];
|
|
312
|
+
const all = [...new Set(texts.flatMap((text) => placeholdersIn(inLanguage(text, lang))))];
|
|
313
|
+
return { paths: all.filter((path) => path !== "answer"), answer: all.includes("answer") };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** A cell of a table of tasks: on one line, without the table's separator. */
|
|
317
|
+
const cell = (text: string) => text.replace(/\s+/g, " ").replace(/\|/g, "/").trim() || "–";
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* A generated exercise for the tutor, once: its task as a template ({G} for the value of G), its answer format, hint
|
|
321
|
+
* and steps, its typical mistakes as F1, F2 … with the app's message, and the link to a number; then a table of each
|
|
322
|
+
* task number's inputs and outputs: the values of the placeholders, the choices of a choice, the solution and what
|
|
323
|
+
* each mistake gives, all as the learner reads them, so that the tutor fills in and works nothing out. The examples
|
|
324
|
+
* stay out: they are test cases. The link leads to the exercise's page at `page`, from where the tutor page is.
|
|
325
|
+
*/
|
|
326
|
+
export function generatedLines(spec: Generated, site: Site, lang: string, format: string[], page = "../"): string[] {
|
|
327
|
+
const words = wordsIn(lang);
|
|
328
|
+
const shared = tutorWords(lang);
|
|
329
|
+
const lines = [`- ${shared.task}: „${template(spec.task.text, lang)}“`, ...format];
|
|
330
|
+
if (spec.hint !== undefined) lines.push(`- ${shared.hint}: „${template(spec.hint, lang)}“`);
|
|
331
|
+
if (spec.steps !== undefined) {
|
|
332
|
+
lines.push(`- ${words.steps}:`, ...spec.steps.map((step) => ` - ${template(step, lang)}`));
|
|
333
|
+
}
|
|
334
|
+
const mistakes = spec.mistakes ?? [];
|
|
335
|
+
if (mistakes.length > 0) {
|
|
336
|
+
lines.push(
|
|
337
|
+
`- ${shared.mistakes}:`,
|
|
338
|
+
...mistakes.map((mistake, index) => ` - F${index + 1}: „${template(mistake.message, lang)}“`),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
lines.push(`- ${words.linkNumber}: \`${page}?${spec.id}=n&via=tutor#${spec.id}\``);
|
|
342
|
+
const { paths, answer } = placeholdersOf(spec, lang);
|
|
343
|
+
const data = dataOf(spec, site.data);
|
|
344
|
+
const templates = templatesOf(spec, [lang]);
|
|
345
|
+
let choices = false;
|
|
346
|
+
const rows = taskSeeds(spec, data).map((seed, index) => {
|
|
347
|
+
const task = generate(spec, seed, data);
|
|
348
|
+
const question = questionFor(spec, task, lang, templates);
|
|
349
|
+
const options =
|
|
350
|
+
question.type === "single-choice" || question.type === "multiple-choice"
|
|
351
|
+
? [cell(question.options.map((option) => plainText(option.text)).join("; "))]
|
|
352
|
+
: [];
|
|
353
|
+
if (options.length > 0) choices = true;
|
|
354
|
+
const wrong = mistakes.map((_mistake, at) => {
|
|
355
|
+
try {
|
|
356
|
+
return cell(label(task.mistakes[at] ?? "", lang));
|
|
357
|
+
} catch {
|
|
358
|
+
return "–";
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
return [
|
|
362
|
+
String(index + 1),
|
|
363
|
+
...paths.map((path) => cell(placeholderValue(spec, task, path, lang)?.text ?? "")),
|
|
364
|
+
...options,
|
|
365
|
+
cell(plainText(answerText(question, lang))),
|
|
366
|
+
...wrong,
|
|
367
|
+
].join(" | ");
|
|
368
|
+
});
|
|
369
|
+
const header = [
|
|
370
|
+
words.number,
|
|
371
|
+
...paths,
|
|
372
|
+
...(choices ? [words.choices] : []),
|
|
373
|
+
shared.solution,
|
|
374
|
+
...mistakes.map((_mistake, index) => `F${index + 1}`),
|
|
375
|
+
].join(" | ");
|
|
376
|
+
lines.push(`- ${words.table}${answer ? ` ${words.answerIs}` : ""}`, "", "```", header, ...rows, "```");
|
|
377
|
+
return lines;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The table of tasks of an exercise component, as a generated exercise has it: each task number's inputs, its
|
|
382
|
+
* solution and what each typical mistake gives, all as the learner reads them ("–" for a mistake without one answer).
|
|
383
|
+
*/
|
|
384
|
+
function componentTable(exercise: ComponentExercise, lang: string): string[] {
|
|
385
|
+
const words = wordsIn(lang);
|
|
386
|
+
const count = exercise.tasks[0]?.mistakes.length ?? 0;
|
|
387
|
+
// a typical mistake without a single answer for any number has no column: its description above says what it is
|
|
388
|
+
const columns = Array.from({ length: count }, (_mistake, index) => index).filter((index) =>
|
|
389
|
+
exercise.tasks.some((row) => (row.mistakes[index] ?? "") !== ""),
|
|
390
|
+
);
|
|
391
|
+
// an input the task's text does not show is hidden from the child
|
|
392
|
+
const shown = new Set([...exercise.template.matchAll(/\{([^{}\s]+)\}/g)].map((match) => match[1]));
|
|
393
|
+
const header = [
|
|
394
|
+
words.number,
|
|
395
|
+
...exercise.inputs.map((input) => (shown.has(input) ? input : `${input} ${words.hidden}`)),
|
|
396
|
+
tutorWords(lang).solution,
|
|
397
|
+
...columns.map((index) => `F${index + 1}`),
|
|
398
|
+
].join(" | ");
|
|
399
|
+
const rows = exercise.tasks.map((row, index) =>
|
|
400
|
+
[String(index + 1), ...row.inputs, row.solution, ...columns.map((column) => row.mistakes[column] ?? "")]
|
|
401
|
+
.map(cell)
|
|
402
|
+
.join(" | "),
|
|
403
|
+
);
|
|
404
|
+
return [`- ${words.table}`, "", "```", header, ...rows, "```"];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The hint of the build for the exercise components that declare no table of tasks (TABLE_CONTRACT), each by its
|
|
409
|
+
* place, or none: their tutor pages have their tasks only as text from describe, as many as fit. A hint for now;
|
|
410
|
+
* rule vue-component-tutor-table.
|
|
411
|
+
*/
|
|
412
|
+
export function tableHint(site: Site, descriptions: Descriptions): string | undefined {
|
|
413
|
+
const without = site.components.filter((use) => {
|
|
414
|
+
const description = descriptions.get(use.block);
|
|
415
|
+
return description !== undefined && description.exercise === undefined;
|
|
416
|
+
});
|
|
417
|
+
if (without.length === 0) return undefined;
|
|
418
|
+
const one = without.length === 1;
|
|
419
|
+
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)`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** An exercise with numbers left out of a tutor page for its size: how many of its numbers it shows. */
|
|
423
|
+
export interface Cut {
|
|
424
|
+
id: string;
|
|
425
|
+
shown: number;
|
|
426
|
+
count: number;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The tutor page of `page` in `lang`, as Markdown, its links relative to the page's folder <chapter>/<page>/tutor/,
|
|
431
|
+
* which the build checks: who the tutor helps, how to guide, where the child is, the app's own rules; then the page's
|
|
432
|
+
* content as context, and its exercises. An exercise component shows its table of tasks with every number, or without
|
|
433
|
+
* one each number's task and solution; when the page would grow beyond `budget`, the components without a table share
|
|
434
|
+
* what is left, number by number in turn, and `cut` names those with numbers left out.
|
|
435
|
+
*/
|
|
436
|
+
export function tutorPage(
|
|
437
|
+
site: Site,
|
|
438
|
+
page: Page,
|
|
439
|
+
lang: string,
|
|
440
|
+
descriptions: Descriptions,
|
|
441
|
+
budget: number = TUTOR_PAGE_BUDGET,
|
|
442
|
+
): { markdown: string; cut: Cut[] } {
|
|
443
|
+
const words = wordsIn(lang);
|
|
444
|
+
const exercises = pageExercises(site, page, descriptions);
|
|
445
|
+
// the task of each number of each exercise component, from its describe
|
|
446
|
+
const rows = new Map<string, string[]>();
|
|
447
|
+
for (const exercise of exercises) {
|
|
448
|
+
if (exercise.block.type !== "vue-component" || exercise.count === 0) continue;
|
|
449
|
+
// a component with its table of tasks has all its numbers there
|
|
450
|
+
if (descriptions.get(exercise.block)?.exercise?.[lang] !== undefined) continue;
|
|
451
|
+
const tasks = descriptions.get(exercise.block)?.tasks[lang] ?? [];
|
|
452
|
+
rows.set(
|
|
453
|
+
exercise.id,
|
|
454
|
+
tasks.map(
|
|
455
|
+
(task) =>
|
|
456
|
+
` - ${words.number} ${task.number}: ${line(task.task)} → ${tutorWords(lang).solution}: ${line(task.solution)}`,
|
|
457
|
+
),
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
const shown = new Map<string, number>();
|
|
461
|
+
const build = () => pageMarkdown(site, page, lang, descriptions, exercises, rows, shown);
|
|
462
|
+
const size = (text: string) => bytesOf(tutorPageDocument(text, site, page, lang));
|
|
463
|
+
let markdown = build();
|
|
464
|
+
if (size(markdown) > budget && rows.size > 0) {
|
|
465
|
+
for (const id of rows.keys()) shown.set(id, 0);
|
|
466
|
+
let room = budget - size(build());
|
|
467
|
+
// a row's share of the page: its HTML, a list item
|
|
468
|
+
const cost = (row: string) => bytesOf(escape(row)) + "<li></li>\n".length;
|
|
469
|
+
for (let more = true; more;) {
|
|
470
|
+
more = false;
|
|
471
|
+
for (const [id, all] of rows) {
|
|
472
|
+
const next = all[shown.get(id) ?? 0];
|
|
473
|
+
if (next === undefined || cost(next) > room) continue;
|
|
474
|
+
room -= cost(next);
|
|
475
|
+
shown.set(id, (shown.get(id) ?? 0) + 1);
|
|
476
|
+
more = true;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
markdown = build();
|
|
480
|
+
// the cost of a row is a guess: one row less of the largest until the page fits
|
|
481
|
+
while (size(markdown) > budget) {
|
|
482
|
+
const [largest] = [...shown.entries()].sort((a, b) => b[1] - a[1]);
|
|
483
|
+
if (largest === undefined || largest[1] === 0) break;
|
|
484
|
+
shown.set(largest[0], largest[1] - 1);
|
|
485
|
+
markdown = build();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const cut = [...rows].flatMap(([id, all]) => {
|
|
489
|
+
const n = shown.get(id) ?? all.length;
|
|
490
|
+
return n < all.length ? [{ id, shown: n, count: all.length }] : [];
|
|
491
|
+
});
|
|
492
|
+
return { markdown, cut };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The learner's needs on a tutor page, in `lang`, after its guide (dec-tutor-needs): what holds when the query names
|
|
497
|
+
* them in `bedarf=`, then a short section for each, as Markdown lines.
|
|
498
|
+
*/
|
|
499
|
+
export function needsLines(lang: string): string[] {
|
|
500
|
+
const words = wordsIn(lang);
|
|
501
|
+
return [
|
|
502
|
+
`## ${words.needsTitle}`,
|
|
503
|
+
"",
|
|
504
|
+
words.needsIntro,
|
|
505
|
+
"",
|
|
506
|
+
...NEEDS.map((need) => `- \`${need}\`: ${words.needs[need]}`),
|
|
507
|
+
"",
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** The tutor page of `page` in `lang`, as Markdown (tutorPage). */
|
|
512
|
+
export const tutorPageMarkdown = (site: Site, page: Page, lang: string, descriptions: Descriptions): string =>
|
|
513
|
+
tutorPage(site, page, lang, descriptions).markdown;
|
|
514
|
+
|
|
515
|
+
/** The Markdown of a tutor page, with of each exercise component the rows by number `shown` names (all if none). */
|
|
516
|
+
function pageMarkdown(
|
|
517
|
+
site: Site,
|
|
518
|
+
page: Page,
|
|
519
|
+
lang: string,
|
|
520
|
+
descriptions: Descriptions,
|
|
521
|
+
exercises: readonly PageExercise[],
|
|
522
|
+
rows: ReadonlyMap<string, readonly string[]>,
|
|
523
|
+
shown: ReadonlyMap<string, number>,
|
|
524
|
+
): string {
|
|
525
|
+
const words = wordsIn(lang);
|
|
526
|
+
const own = (text: Text) => line(inLanguage(text, lang));
|
|
527
|
+
const title = own(page.title);
|
|
528
|
+
const fill = (text: string) => text.replace("{page}", title).replace("{app}", own(site.app.title));
|
|
529
|
+
const lines = [
|
|
530
|
+
`# ${fill(words.title)}`,
|
|
531
|
+
"",
|
|
532
|
+
fill(words.intro),
|
|
533
|
+
"",
|
|
534
|
+
`${words.parents} [${words.toPage}](../)`,
|
|
535
|
+
"",
|
|
536
|
+
...(site.app.scope === undefined ? [] : [words.scope, ""]),
|
|
537
|
+
`## ${words.guideTitle}`,
|
|
538
|
+
"",
|
|
539
|
+
...words.guide.map((text) => `- ${text}`),
|
|
540
|
+
"",
|
|
541
|
+
`## ${words.startTitle}`,
|
|
542
|
+
"",
|
|
543
|
+
...words.start.map((text) => `- ${text}`),
|
|
544
|
+
"",
|
|
545
|
+
];
|
|
546
|
+
if (site.app.tutor?.rules !== undefined) {
|
|
547
|
+
lines.push(`## ${words.appRules}`, "", inLanguage(site.app.tutor.rules, lang).trim(), "");
|
|
548
|
+
}
|
|
549
|
+
// where the child is and the link of a task, as examples: of the page's first exercise with numbers if any
|
|
550
|
+
const first = exercises.find((exercise) => exercise.count > 0) ?? exercises[0];
|
|
551
|
+
const example =
|
|
552
|
+
first === undefined
|
|
553
|
+
? undefined
|
|
554
|
+
: first.count > 0
|
|
555
|
+
? { query: `${first.id}=12`, link: `…/${page.chapter}/${page.id}/?${first.id}=12&via=tutor#${first.id}` }
|
|
556
|
+
: { query: `uebung=${first.id}`, link: `…/${page.chapter}/${page.id}/?via=tutor#${first.id}` };
|
|
557
|
+
lines.push(
|
|
558
|
+
`## ${words.whereTitle}`,
|
|
559
|
+
"",
|
|
560
|
+
...words.where.flatMap((text) =>
|
|
561
|
+
!text.includes("{example}") && !text.includes("{link}")
|
|
562
|
+
? [`- ${text}`]
|
|
563
|
+
: example === undefined
|
|
564
|
+
? []
|
|
565
|
+
: [`- ${text.replace("{example}", example.query).replace("{link}", example.link)}`],
|
|
566
|
+
),
|
|
567
|
+
"",
|
|
568
|
+
...needsLines(lang),
|
|
569
|
+
`## ${fill(words.contextTitle)}`,
|
|
570
|
+
"",
|
|
571
|
+
);
|
|
572
|
+
if (page.description !== undefined) lines.push(own(page.description), "");
|
|
573
|
+
lines.push(
|
|
574
|
+
...contentLines(
|
|
575
|
+
site,
|
|
576
|
+
page.blocks,
|
|
577
|
+
lang,
|
|
578
|
+
formulas,
|
|
579
|
+
(heading, _anchor, depth) => `${"#".repeat(Math.min(3 + depth, 6))} ${heading}`,
|
|
580
|
+
),
|
|
581
|
+
);
|
|
582
|
+
for (const exercise of exercises) {
|
|
583
|
+
const part = exercisePart(site, exercise.block, lang, descriptions);
|
|
584
|
+
if (part === undefined) continue;
|
|
585
|
+
lines.push(`### ${words.exercise} ${part.id}`, "");
|
|
586
|
+
if (exercise.block.type === "generated") {
|
|
587
|
+
const format = part.lines.filter((text) => text.startsWith(`- ${tutorWords(lang).answerFormat}:`)).map(line);
|
|
588
|
+
lines.push(...generatedLines(exercise.block, site, lang, format));
|
|
589
|
+
} else {
|
|
590
|
+
lines.push(
|
|
591
|
+
...part.lines.map((text) => {
|
|
592
|
+
const [, indent = "", rest = ""] = /^(\s*- )?(.*)$/.exec(text) ?? [];
|
|
593
|
+
return `${indent}${line(rest)}`;
|
|
594
|
+
}),
|
|
595
|
+
);
|
|
596
|
+
const declared = descriptions.get(exercise.block)?.exercise?.[lang];
|
|
597
|
+
if (exercise.count > 0) {
|
|
598
|
+
lines.push(`- ${words.linkNumber}: \`../?${exercise.id}=n&via=tutor#${exercise.id}\``);
|
|
599
|
+
const all = rows.get(exercise.id) ?? [];
|
|
600
|
+
const n = shown.get(exercise.id) ?? all.length;
|
|
601
|
+
if (declared !== undefined) lines.push(...componentTable(declared, lang));
|
|
602
|
+
else if (n > 0) lines.push(`- ${words.tasks}:`, ...all.slice(0, n));
|
|
603
|
+
if (n === 0 && all.length > 0) lines.push(`- ${words.none}`);
|
|
604
|
+
else if (n < all.length) {
|
|
605
|
+
lines.push(`- ${words.shown.replace("{shown}", String(n)).replace("{count}", String(all.length))}`);
|
|
606
|
+
}
|
|
607
|
+
} else lines.push(`- ${words.link}: [../?via=tutor#${exercise.id}](../?via=tutor#${exercise.id})`);
|
|
608
|
+
}
|
|
609
|
+
lines.push("");
|
|
610
|
+
}
|
|
611
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** A formula $…$ of the page's Markdown, kept as written: neither emphasis nor escapes inside it. */
|
|
615
|
+
const FORMULA = {
|
|
616
|
+
name: "formula",
|
|
617
|
+
level: "inline" as const,
|
|
618
|
+
start: (source: string) => {
|
|
619
|
+
const at = source.indexOf("$");
|
|
620
|
+
return at < 0 ? undefined : at;
|
|
621
|
+
},
|
|
622
|
+
tokenizer: (source: string) => {
|
|
623
|
+
const match = /^\$(?:\\.|[^\\$\n])+?\$/.exec(source);
|
|
624
|
+
return match === null ? undefined : { type: "formula", raw: match[0] };
|
|
625
|
+
},
|
|
626
|
+
renderer: (token: { raw: string }) => escape(token.raw),
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const markdown = new Marked({ gfm: true, extensions: [FORMULA] });
|
|
630
|
+
|
|
631
|
+
/** The tutor page of `page` in `lang` as a whole HTML document: lean, readable at any width, without script. */
|
|
632
|
+
export function tutorPageHtml(site: Site, page: Page, lang: string, descriptions: Descriptions): string {
|
|
633
|
+
return tutorPageDocument(tutorPageMarkdown(site, page, lang, descriptions), site, page, lang);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** The HTML document of a tutor page's Markdown `text`. */
|
|
637
|
+
export function tutorPageDocument(text: string, site: Site, page: Page, lang: string): string {
|
|
638
|
+
const words = wordsIn(lang);
|
|
639
|
+
const own = (value: Text) => inLanguage(value, lang).replace(/\s+/g, " ").trim();
|
|
640
|
+
return tutorDocument(
|
|
641
|
+
text,
|
|
642
|
+
`${own(page.title)} – ${words.tutorPage} – ${own(site.app.title)}`,
|
|
643
|
+
words.forTutor.replace("{page}", own(page.title)),
|
|
644
|
+
lang,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** The HTML document of a tutor page's Markdown `text`, with its `title` and `description`: lean, without script. */
|
|
649
|
+
export function tutorDocument(text: string, title: string, description: string, lang: string): string {
|
|
650
|
+
const body = markdown.parse(text, { async: false }).trim();
|
|
651
|
+
return [
|
|
652
|
+
"<!doctype html>",
|
|
653
|
+
`<html lang="${lang}">`,
|
|
654
|
+
"<head>",
|
|
655
|
+
'<meta charset="utf-8" />',
|
|
656
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
657
|
+
`<title>${escape(title)}</title>`,
|
|
658
|
+
`<meta name="description" content="${escape(description)}" />`,
|
|
659
|
+
"<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>",
|
|
660
|
+
"</head>",
|
|
661
|
+
"<body>",
|
|
662
|
+
'<main id="anleitung">',
|
|
663
|
+
body,
|
|
664
|
+
"</main>",
|
|
665
|
+
"</body>",
|
|
666
|
+
"</html>",
|
|
667
|
+
"",
|
|
668
|
+
].join("\n");
|
|
669
|
+
}
|