@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.
- 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 +142 -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,200 @@
|
|
|
1
|
+
{
|
|
2
|
+
"language": "English",
|
|
3
|
+
"languages": "Language",
|
|
4
|
+
"start": "Start",
|
|
5
|
+
"breadcrumbs": "You are here",
|
|
6
|
+
"contents": "Contents",
|
|
7
|
+
"onThisPage": "On this page",
|
|
8
|
+
"pages": "Pages",
|
|
9
|
+
"chapters": "Chapters",
|
|
10
|
+
"pager": "Page through",
|
|
11
|
+
"previous": "Back: {title}",
|
|
12
|
+
"next": "Next: {title}",
|
|
13
|
+
"glossary": "Glossary",
|
|
14
|
+
"glossaryIntro": "The terms of this app, from A to Z, each with the page that explains it.",
|
|
15
|
+
"inGlossary": "in the glossary",
|
|
16
|
+
"explainedOn": "Explained on the page {page}",
|
|
17
|
+
"sources": "Sources",
|
|
18
|
+
"sourcesIntro": "The facts in this app come from these sources:",
|
|
19
|
+
"beforeChrist": "{year} BC",
|
|
20
|
+
"code": "Code in {language}",
|
|
21
|
+
"footer": "An explainer on lernapps.net",
|
|
22
|
+
"imprint": "Imprint",
|
|
23
|
+
"privacy": "Privacy",
|
|
24
|
+
"workedTasks": "Three worked tasks, each with its solution:",
|
|
25
|
+
"practice": "Practise yourself, with new tasks each time:",
|
|
26
|
+
"newTask": "New task",
|
|
27
|
+
"hint": "Hint",
|
|
28
|
+
"textDescription": "Text description",
|
|
29
|
+
"taskNumber": "Task no. {n}",
|
|
30
|
+
"backToTutor": "Back to the tutor",
|
|
31
|
+
"backToTutorOpen": "Switch back to the chat with {name}.",
|
|
32
|
+
"backToTutorCopied": "Switch back to {name} and paste it there. You may leave this tab open.",
|
|
33
|
+
"backToTutorFailed": "Copying did not work. Copy the selected lines, switch back to {name} and paste them there.",
|
|
34
|
+
"resultLabel": "Result line for your tutor",
|
|
35
|
+
"resultLine": "{page}, {exercise}{number}: {status}.",
|
|
36
|
+
"resultNumber": ", no. {n}",
|
|
37
|
+
"resultRight": "right on try {n}",
|
|
38
|
+
"resultSolution": "solution viewed",
|
|
39
|
+
"resultOpen": "not solved yet",
|
|
40
|
+
"resultTry": "1 try",
|
|
41
|
+
"resultTries": "{n} tries",
|
|
42
|
+
"resultInput": "input {input}",
|
|
43
|
+
"resultSession": "Session: {right} of {total} right.",
|
|
44
|
+
"resultTutor": "Tutor page:",
|
|
45
|
+
"resultTrue": "true",
|
|
46
|
+
"resultFalse": "false",
|
|
47
|
+
"stuckAsk": "Load {url} and help me with exercise {exercise}{number}. I am stuck.",
|
|
48
|
+
"stuckTask": "Exercise {exercise}{number}:",
|
|
49
|
+
"stuckInput": "I entered {input} ({tries}).",
|
|
50
|
+
"stuckTries": "So far {tries}.",
|
|
51
|
+
"stuckRight": "I solved it on try {n}.",
|
|
52
|
+
"stuckMessage": "The app says: “{message}”",
|
|
53
|
+
"stuckHint": "I opened the hint.",
|
|
54
|
+
"stuckSolution": "I viewed the solution.",
|
|
55
|
+
"stuckPrivacy": "With your click, “Why?” gives {name} the task, your input and the app's message.",
|
|
56
|
+
"stuckPrivacyCopy": "Your click copies the task, your input and the app's message for the chat with your tutor; the app sends nothing anywhere.",
|
|
57
|
+
"stuckCopied": "Switch back to {name} and paste the lines there.",
|
|
58
|
+
"stuckFailed": "Copying did not work. Copy the selected lines, switch back to {name} and paste them there.",
|
|
59
|
+
"stuckLabel": "Lines for your tutor",
|
|
60
|
+
"tutorTitle": "Learn with an AI tutor",
|
|
61
|
+
"tutorAbout": "An AI tutor is an AI assistant of your choice that accompanies you while you learn with this app. It explains the topic with the examples of the pages, gives you tasks from the app and helps you when you are stuck. You solve the tasks in the app: it checks your answers.",
|
|
62
|
+
"tutorAboutStart": "Choose your assistant here. It is then ready under the title of every page, and every exercise has the button “Help”. You can also switch it on under the title of a page.",
|
|
63
|
+
"tutorAboutPrivacy": "Your choice stays on this device; only your click opens the assistant. Ask your parents or teacher whether you may use it.",
|
|
64
|
+
"tutorAccount": "You use your own account with the provider. The assistant is not offered by lernapps.net: lernapps only provides the content and the exercises.",
|
|
65
|
+
"tutorScope": "The choice holds for all pages, on this device only.",
|
|
66
|
+
"tutorChoose": "Your AI assistant",
|
|
67
|
+
"tutorNone": "None",
|
|
68
|
+
"needsTitle": "What helps you learn",
|
|
69
|
+
"needsOpen": "Settings",
|
|
70
|
+
"needsDone": "Done",
|
|
71
|
+
"needsAbout": "Here you can tell your tutor how it should talk to you. Tick what helps you; you can choose several.",
|
|
72
|
+
"needKurzeSchritte": "Short steps",
|
|
73
|
+
"needKurzeSchritteAbout": "The tutor explains in small steps, only one thing at a time. You always know how far there is still to go.",
|
|
74
|
+
"needWenigLesen": "Little to read",
|
|
75
|
+
"needWenigLesenAbout": "The tutor writes very briefly and simply and explains every technical term. Tip: let it read its answers out to you and speak your answer.",
|
|
76
|
+
"needMehrTiefe": "More depth",
|
|
77
|
+
"needMehrTiefeAbout": "The tutor skips what you already know and asks you tricky why and what-if questions.",
|
|
78
|
+
"needAndereMuttersprache": "Another first language",
|
|
79
|
+
"needAndereMutterspracheAbout": "The tutor explains in simple sentences in the language of the app and translates new technical terms into your language. You may also write in your language.",
|
|
80
|
+
"needKlarUndWoertlich": "Clear and literal",
|
|
81
|
+
"needKlarUndWoertlichAbout": "The tutor writes exactly what it means: no jokes, hidden meanings or idioms. It always tells you where you are.",
|
|
82
|
+
"needRechnenAnschaulich": "Numbers you can see",
|
|
83
|
+
"needRechnenAnschaulichAbout": "The tutor shows you how much a number is and explains in small steps. You work things out yourself and take as much time as you need.",
|
|
84
|
+
"needsLanguage": "Your first language",
|
|
85
|
+
"needsLanguageNone": "Please choose",
|
|
86
|
+
"needsScopeTitle": "This holds",
|
|
87
|
+
"needsScopeAll": "for every explainer app on lernapps.net",
|
|
88
|
+
"needsScopeApp": "for this app only",
|
|
89
|
+
"needsPrivacy": "Your choice stays on this device. The tutor gets only the names of the settings you tick, and only when you open it.",
|
|
90
|
+
"tutorOther": "Another one: copy the text",
|
|
91
|
+
"tutorOn": "Switch on",
|
|
92
|
+
"tutorSomeone": "your AI tutor",
|
|
93
|
+
"help": "Help",
|
|
94
|
+
"helpWhy": "Why?",
|
|
95
|
+
"helpBack": "Back",
|
|
96
|
+
"helpLabel": "Ask {name} for help",
|
|
97
|
+
"helpWhyLabel": "Ask {name} why",
|
|
98
|
+
"helpBackLabel": "Back to {name}",
|
|
99
|
+
"tutorCopy": "Copy the text",
|
|
100
|
+
"tutorOpenWith": "Open with {name}",
|
|
101
|
+
"tutorGo": "Learn with {name}",
|
|
102
|
+
"tutorWay": "Assistant",
|
|
103
|
+
"tutorPageIntro": "An AI tutor explains this page to you and helps you with the exercises. Choose your assistant:",
|
|
104
|
+
"tutorPageLink": "See what your tutor gets for this page",
|
|
105
|
+
"tutorPasteHint": "{name} does not load web pages when the link comes from a button. So we copy the sentence with the link for you. A click on “Learn with {name}” opens {name} with a short message. Then paste the copied sentence (Ctrl+V, ⌘+V on a Mac) and send it.",
|
|
106
|
+
"tutorDone": "✓ Copied – now paste it into {name}",
|
|
107
|
+
"tutorCopied": "The prompt is copied. Paste it into the chat of your AI assistant.",
|
|
108
|
+
"tutorCopyFailed": "Copying did not work. Try again.",
|
|
109
|
+
"exerciseLabel": "Exercise",
|
|
110
|
+
"videoChannel": "{channel} · YouTube",
|
|
111
|
+
"videoNote": "Playing it sends data to YouTube/Google, among them your IP address.",
|
|
112
|
+
"videoPlay": "Play video",
|
|
113
|
+
"videoAlways": "Always load videos directly",
|
|
114
|
+
"assessmentTitle": "Self-assessment",
|
|
115
|
+
"assessmentOfferTitle": "What do you know already?",
|
|
116
|
+
"assessmentOfferApp": "Optional: before you start, you can solve {count} tasks from this app. At the end you see for each page whether you should look at it more closely.",
|
|
117
|
+
"assessmentOfferChapter": "Optional: before you start, you can solve {count} tasks from this chapter. At the end you see for each page whether you should look at it more closely.",
|
|
118
|
+
"assessmentOfferStart": "Start the self-assessment",
|
|
119
|
+
"assessmentOfferLast": "See your last result",
|
|
120
|
+
"assessmentIntro": "With this self-assessment, {count} tasks give you an overview of what you know already.",
|
|
121
|
+
"assessmentAllAsked": "It asks about every page.",
|
|
122
|
+
"assessmentLeftOut": "It does not ask about {n} pages. So they are worth a look:",
|
|
123
|
+
"assessmentLeftOutOne": "It does not ask about one page. So it is worth a look:",
|
|
124
|
+
"assessmentHow": "The app does not tell you for each task whether it is right. Only at the end do you see for each page asked whether you know it already. What you leave empty counts as not known.",
|
|
125
|
+
"assessmentPrivacy": "Your result stays on this device only. A new start replaces it.",
|
|
126
|
+
"assessmentBegin": "Let's go",
|
|
127
|
+
"assessmentStep": "Page {n} of {count}",
|
|
128
|
+
"assessmentTask": "Task {n}",
|
|
129
|
+
"assessmentBack": "Back",
|
|
130
|
+
"assessmentNext": "Next",
|
|
131
|
+
"assessmentFinish": "Evaluate",
|
|
132
|
+
"assessmentResultTitle": "Your result",
|
|
133
|
+
"assessmentScore": "You solved {right} of {total} tasks right.",
|
|
134
|
+
"assessmentScorePartly": "You solved {right} of {total} tasks right and {partly} partly.",
|
|
135
|
+
"assessmentAdvice": "Start with the pages you should read closely.",
|
|
136
|
+
"assessmentAllKnown": "You probably know the pages asked already. Practise on the pages anyway, so that it sticks.",
|
|
137
|
+
"assessmentKnown": "You probably know it already",
|
|
138
|
+
"assessmentPartly": "Look at it again",
|
|
139
|
+
"assessmentRead": "Read it closely",
|
|
140
|
+
"assessmentUnasked": "Not asked",
|
|
141
|
+
"assessmentPageScore": "{right} of {total} right",
|
|
142
|
+
"assessmentPageScorePartly": "{right} of {total} right, {partly} partly",
|
|
143
|
+
"assessmentRestart": "Start again",
|
|
144
|
+
"assessmentDelete": "Delete the result",
|
|
145
|
+
"assessmentDeleted": "The result is deleted.",
|
|
146
|
+
"assessmentTutorTitle": "Go on learning with an AI tutor",
|
|
147
|
+
"assessmentTutorIntro": "With your result, an AI tutor can go through the pages with you that do not stick yet. Your click gives it the result and your inputs; apart from that the app sends nothing.",
|
|
148
|
+
"assessmentTutorGo": "Talk it over with {name}",
|
|
149
|
+
"assessmentTutorPage": "See what your tutor gets",
|
|
150
|
+
"assessmentNojs": "The self-assessment needs JavaScript.",
|
|
151
|
+
"assessmentAsk": "Load {url} and be my tutor. I did the self-assessment. My result per page:",
|
|
152
|
+
"assessmentLine": "{key} ({page}, exercise {exercise}{number}): {state}.",
|
|
153
|
+
"assessmentRight": "right",
|
|
154
|
+
"assessmentPartlyRight": "partly right",
|
|
155
|
+
"assessmentWrong": "wrong",
|
|
156
|
+
"assessmentOpen": "not answered",
|
|
157
|
+
"assessmentInput": "Input: {input}",
|
|
158
|
+
"assessmentMissed": "Not reached: {points}.",
|
|
159
|
+
"assessmentPageLine": "“{page}”: {band} ({right} of {total} right).",
|
|
160
|
+
"assessmentPageLinePartly": "“{page}”: {band} ({right} of {total} right, {partly} partly).",
|
|
161
|
+
"assessmentPageUnasked": "“{page}”: not asked.",
|
|
162
|
+
"assessmentTasks": "My tasks:",
|
|
163
|
+
"assessmentBackIntro": "Press “Back to the tutor”. It copies your result. Paste it into the chat with your tutor.",
|
|
164
|
+
"assessmentBackLine": "I did the self-assessment. My result per page:",
|
|
165
|
+
"assessmentTutorRun": "This round is for your tutor: the app keeps nothing of it.",
|
|
166
|
+
"sessionIntro": "I practised on {url}:",
|
|
167
|
+
"sessionNone": "I have not solved a task on {url} yet.",
|
|
168
|
+
"sessionTask": "{exercise}{number}: {status}.",
|
|
169
|
+
"assessmentNextRound": "Link for a new round: {url}",
|
|
170
|
+
"assessmentLinks": "Links for my tutor, take exactly these:",
|
|
171
|
+
"assessmentLinkChapter": "Chapter “{chapter}”, self-assessment: {url}",
|
|
172
|
+
"assessmentLinkChapterNone": "Chapter “{chapter}” (no self-assessment):",
|
|
173
|
+
"assessmentLinkPage": "“{page}”: {url}",
|
|
174
|
+
"assessmentLinkWhole": "Self-assessment of the whole app: {url}",
|
|
175
|
+
"backToTutorDone": "✓ Copied",
|
|
176
|
+
"tutorSomeoneTo": "your AI tutor",
|
|
177
|
+
"openTaskLabel": "Open task",
|
|
178
|
+
"openTaskPoints": "A good answer has:",
|
|
179
|
+
"openTaskForm": "form",
|
|
180
|
+
"openTaskSource": "Read up",
|
|
181
|
+
"openTaskChoice": "Your answer",
|
|
182
|
+
"openTaskType": "Type it here",
|
|
183
|
+
"openTaskPaper": "On paper, then a photo",
|
|
184
|
+
"openTaskAnswer": "Your answer",
|
|
185
|
+
"openTaskPaperHow": "Write your answer on a sheet of paper. Then take a photo of it.",
|
|
186
|
+
"openTaskPhoto": "Take a photo of the sheet",
|
|
187
|
+
"openTaskPhotoAgain": "New photo",
|
|
188
|
+
"openTaskPhotoAlt": "Photo of your sheet",
|
|
189
|
+
"openTaskPhotoLocal": "The photo stays on this device: the app neither keeps nor sends it.",
|
|
190
|
+
"openTaskCompare": "Compare",
|
|
191
|
+
"openTaskWriteFirst": "Write your answer first. Then you can compare.",
|
|
192
|
+
"openTaskPhotoFirst": "Take a photo of your sheet first. Then you can compare.",
|
|
193
|
+
"openTaskCheck": "What is in your answer?",
|
|
194
|
+
"openTaskCheckHow": "Tick what you really wrote.",
|
|
195
|
+
"openTaskChange": "Change the answer",
|
|
196
|
+
"openTaskLocked": "The solution opens after your answer.",
|
|
197
|
+
"resultPartly": "partly right ({n} of {total} points, missing {missed})",
|
|
198
|
+
"resultPhoto": "answer as a photo",
|
|
199
|
+
"resultAnswer": "answer “{input}”"
|
|
200
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The explainer's own texts for learners, in each language it has them: an app may declare only these languages.
|
|
2
|
+
import de from "./messages/de.json" with { type: "json" };
|
|
3
|
+
import en from "./messages/en.json" with { type: "json" };
|
|
4
|
+
import { textsIn, type Language } from "../shared/languages.ts";
|
|
5
|
+
|
|
6
|
+
export type Messages = typeof de;
|
|
7
|
+
export const MESSAGES: Readonly<Record<Language, Messages>> = { de, en };
|
|
8
|
+
|
|
9
|
+
/** The text of `key` in `lang`, with its placeholders filled. */
|
|
10
|
+
export function message(lang: string, key: keyof Messages, params: Readonly<Record<string, string>> = {}): string {
|
|
11
|
+
const text = textsIn(MESSAGES, lang)[key];
|
|
12
|
+
return text.replace(/\{(\w+)\}/g, (placeholder, name: string) => params[name] ?? placeholder);
|
|
13
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// The needs a learner can choose for the tutor (dec-tutor-needs): ways the tutor talks that help some learners, named
|
|
2
|
+
// by what helps, never by a diagnosis. The learner chooses none, one or several on the start page, and with
|
|
3
|
+
// `andere-muttersprache` their first language; the choice is kept on the device as a setting for every explainer or
|
|
4
|
+
// for this app only (@lernapps/tooling/storage, createSettings). The ways to the tutor that start a chat name the needs
|
|
5
|
+
// in the query of the tutor page (position.ts, `bedarf=`), and the start prompt adds the first language in the
|
|
6
|
+
// learner's voice; the tutor page holds a short section for each need (tutorpage.ts). Pure and without Node: the
|
|
7
|
+
// client module and the build call it.
|
|
8
|
+
|
|
9
|
+
/** The needs, in the order the start page shows them; their ids are a contract with every tutor. */
|
|
10
|
+
export const NEEDS = [
|
|
11
|
+
"kurze-schritte",
|
|
12
|
+
"wenig-lesen",
|
|
13
|
+
"mehr-tiefe",
|
|
14
|
+
"andere-muttersprache",
|
|
15
|
+
"klar-und-woertlich",
|
|
16
|
+
"rechnen-anschaulich",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export type Need = (typeof NEEDS)[number];
|
|
20
|
+
|
|
21
|
+
/** The need whose choice also asks for the learner's first language. */
|
|
22
|
+
export const OTHER_LANGUAGE: Need = "andere-muttersprache";
|
|
23
|
+
|
|
24
|
+
/** The key of the choice among the learner's settings, for every explainer or for one app. */
|
|
25
|
+
export const NEEDS_KEY = "tutor-bedarf";
|
|
26
|
+
|
|
27
|
+
/** What the learner chose: the needs, and their first language (a language tag) with `andere-muttersprache`. */
|
|
28
|
+
export interface NeedsChoice {
|
|
29
|
+
needs: Need[];
|
|
30
|
+
language?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isNeed = (value: unknown): value is Need => NEEDS.some((need) => need === value);
|
|
34
|
+
|
|
35
|
+
/** Whether a stored value is a choice of needs. */
|
|
36
|
+
export function isNeedsChoice(value: unknown): value is NeedsChoice {
|
|
37
|
+
if (typeof value !== "object" || value === null) return false;
|
|
38
|
+
const { needs, language } = value as Record<string, unknown>;
|
|
39
|
+
return Array.isArray(needs) && needs.every(isNeed) && (language === undefined || typeof language === "string");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The first languages the start page offers, as language tags: the most common first languages of children in
|
|
44
|
+
* German schools besides German, and German Sign Language (`gsg`).
|
|
45
|
+
*/
|
|
46
|
+
export const LANGUAGES: readonly string[] = [
|
|
47
|
+
"ar",
|
|
48
|
+
"bg",
|
|
49
|
+
"bs",
|
|
50
|
+
"en",
|
|
51
|
+
"fa",
|
|
52
|
+
"fr",
|
|
53
|
+
"gsg",
|
|
54
|
+
"el",
|
|
55
|
+
"it",
|
|
56
|
+
"hr",
|
|
57
|
+
"ku",
|
|
58
|
+
"pl",
|
|
59
|
+
"ps",
|
|
60
|
+
"pt",
|
|
61
|
+
"ro",
|
|
62
|
+
"ru",
|
|
63
|
+
"so",
|
|
64
|
+
"sq",
|
|
65
|
+
"sr",
|
|
66
|
+
"es",
|
|
67
|
+
"ti",
|
|
68
|
+
"tr",
|
|
69
|
+
"uk",
|
|
70
|
+
"vi",
|
|
71
|
+
"zh",
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/** The names of the languages Intl does not know, in each language of the explainer. */
|
|
75
|
+
const OWN_NAMES: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
76
|
+
de: { gsg: "Deutsche Gebärdensprache" },
|
|
77
|
+
en: { gsg: "German Sign Language" },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** The name of the language `tag` in `lang`, "Türkisch" for `tr` in German; the tag itself if none is known. */
|
|
81
|
+
export function languageName(tag: string, lang: string): string {
|
|
82
|
+
const own = OWN_NAMES[lang]?.[tag] ?? OWN_NAMES["de"]?.[tag];
|
|
83
|
+
if (own !== undefined) return own;
|
|
84
|
+
try {
|
|
85
|
+
const name = new Intl.DisplayNames([lang], { type: "language", fallback: "none" }).of(tag);
|
|
86
|
+
return name === undefined ? tag : name.charAt(0).toLocaleUpperCase(lang) + name.slice(1);
|
|
87
|
+
} catch {
|
|
88
|
+
return tag;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The query parameter of the needs, `bedarf=kurze-schritte,mehr-tiefe` in the order of NEEDS; "" for none. */
|
|
93
|
+
export function needsParameter(choice: NeedsChoice | undefined): string {
|
|
94
|
+
const needs = NEEDS.filter((need) => choice?.needs.includes(need));
|
|
95
|
+
return needs.length === 0 ? "" : `bedarf=${needs.join(",")}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The query `query` (with its "?" or "") with the needs of `choice` added. */
|
|
99
|
+
export function withNeeds(query: string, choice: NeedsChoice | undefined): string {
|
|
100
|
+
const needs = needsParameter(choice);
|
|
101
|
+
if (needs === "") return query;
|
|
102
|
+
return query === "" ? `?${needs}` : `${query}&${needs}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const FIRST_LANGUAGE: Readonly<Record<string, string>> = {
|
|
106
|
+
de: "Meine Muttersprache ist {language}.",
|
|
107
|
+
en: "My first language is {language}.",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The sentence the start prompt adds in the learner's voice when they chose `andere-muttersprache` with a language,
|
|
112
|
+
* in `lang`: "Meine Muttersprache ist Türkisch."; "" otherwise.
|
|
113
|
+
*/
|
|
114
|
+
export function languageSentence(choice: NeedsChoice | undefined, lang: string): string {
|
|
115
|
+
if (choice?.language === undefined || !choice.needs.includes(OTHER_LANGUAGE)) return "";
|
|
116
|
+
const text = FIRST_LANGUAGE[lang] ?? FIRST_LANGUAGE["de"] ?? "";
|
|
117
|
+
return text.replace("{language}", languageName(choice.language, lang));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A prompt that starts a chat, with the sentence of the learner's first language after it when they chose one. */
|
|
121
|
+
export const withLanguage = (prompt: string, choice: NeedsChoice | undefined, lang: string): string =>
|
|
122
|
+
[prompt, languageSentence(choice, lang)].filter((part) => part !== "").join("\n");
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// An open task in the browser (explainer.ts, assessment-client.ts): the learner answers first, then compares. "Deine
|
|
2
|
+
// Lösung" offers the ways the task allows: typing into a text field, or writing on paper and taking a photo of the
|
|
3
|
+
// sheet. "Vergleichen" opens once there is text or a photo; it shows the points of a good answer as tick boxes ("Was
|
|
4
|
+
// steht in deiner Antwort?"), each point of the content with its link to read up, and on a content page it opens the
|
|
5
|
+
// model answer ("Lösung"), which stays shut before the learner's own answer. Ticking makes the learner read each point;
|
|
6
|
+
// the ticks give right (all), partly right (some) or wrong (none) for the result line, at once, without a step of
|
|
7
|
+
// their own. In a self-assessment there is no model answer: the ticks are the
|
|
8
|
+
// answer, read at "Auswerten" of the whole run. The photo is shown from memory (an object URL), never stored or sent.
|
|
9
|
+
// On a content page the text, the way chosen and the ticks are kept while the tab is open (sessionStorage through
|
|
10
|
+
// @lernapps/tooling/storage, under the app's prefix), so that a reload loses no answer and a new day starts afresh; a
|
|
11
|
+
// self-assessment keeps nothing of its own (each run starts empty).
|
|
12
|
+
import { moveFocus } from "@lernapps/tooling/a11y";
|
|
13
|
+
import { createStorage } from "@lernapps/tooling/storage";
|
|
14
|
+
import { escape } from "../shared/html.ts";
|
|
15
|
+
import type { OpenTaskData } from "./exercises.ts";
|
|
16
|
+
import { message } from "./messages.ts";
|
|
17
|
+
import { SHOWN_EVENT } from "./tasks.ts";
|
|
18
|
+
|
|
19
|
+
/** What became of an open task: the points ticked and missed (each from 0), the text, whether there is a photo. */
|
|
20
|
+
export interface OpenTaskResult {
|
|
21
|
+
state: "richtig" | "teilweise" | "falsch" | "offen";
|
|
22
|
+
ticked: number[];
|
|
23
|
+
missed: number[];
|
|
24
|
+
text?: string;
|
|
25
|
+
photo: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What a content page keeps of an open task while the tab is open. */
|
|
29
|
+
interface Kept {
|
|
30
|
+
way: "text" | "photo";
|
|
31
|
+
text: string;
|
|
32
|
+
ticked: number[];
|
|
33
|
+
compared: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isKept = (value: unknown): value is Kept => {
|
|
37
|
+
if (typeof value !== "object" || value === null) return false;
|
|
38
|
+
const kept = value as Partial<Kept>;
|
|
39
|
+
return (
|
|
40
|
+
(kept.way === "text" || kept.way === "photo") &&
|
|
41
|
+
typeof kept.text === "string" &&
|
|
42
|
+
Array.isArray(kept.ticked) &&
|
|
43
|
+
kept.ticked.every((index) => Number.isInteger(index)) &&
|
|
44
|
+
typeof kept.compared === "boolean"
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface OpenTaskOptions {
|
|
49
|
+
/** In a self-assessment: no model answer. */
|
|
50
|
+
assessment?: boolean;
|
|
51
|
+
/** The learner compared (`attempt`: a new try) or changed a tick (the same try). */
|
|
52
|
+
onResult?: (result: OpenTaskResult, attempt: boolean) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The state of an open task as `ticked` of `total` points give it, and the points missed. */
|
|
56
|
+
export function openTaskState(ticked: readonly number[], total: number): Pick<OpenTaskResult, "state" | "missed"> {
|
|
57
|
+
const missed = Array.from({ length: total }, (_point, index) => index).filter((index) => !ticked.includes(index));
|
|
58
|
+
const state = ticked.length === total ? "richtig" : ticked.length === 0 ? "falsch" : "teilweise";
|
|
59
|
+
return { state, missed };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Makes the open task in `element` live; returns what became of it so far. */
|
|
63
|
+
export function startOpenTask(
|
|
64
|
+
element: HTMLElement,
|
|
65
|
+
data: OpenTaskData,
|
|
66
|
+
options: OpenTaskOptions = {},
|
|
67
|
+
): { result: () => OpenTaskResult } {
|
|
68
|
+
const { lang, id, points } = data;
|
|
69
|
+
const t = (key: Parameters<typeof message>[1], params?: Record<string, string>) => escape(message(lang, key, params));
|
|
70
|
+
const live = element.querySelector<HTMLElement>(".explainer-open-live");
|
|
71
|
+
const listed = element.querySelector<HTMLElement>(".explainer-open-points");
|
|
72
|
+
const solution = element.querySelector<HTMLDetailsElement>(":scope > details.explainer-solution");
|
|
73
|
+
const empty = (): OpenTaskResult => ({ state: "offen", ticked: [], missed: [], photo: false });
|
|
74
|
+
if (!live) return { result: empty };
|
|
75
|
+
// with JavaScript the learner answers before seeing the points and the model answer
|
|
76
|
+
if (listed) listed.hidden = true;
|
|
77
|
+
if (solution) solution.hidden = true;
|
|
78
|
+
|
|
79
|
+
const both = data.answer.includes("text") && data.answer.includes("photo");
|
|
80
|
+
let way: "text" | "photo" = data.answer.includes("text") ? "text" : "photo";
|
|
81
|
+
let text = "";
|
|
82
|
+
let photo: string | undefined;
|
|
83
|
+
let ticked: number[] = [];
|
|
84
|
+
let compared = false;
|
|
85
|
+
// kept while the tab is open, by the page and the exercise; a self-assessment keeps nothing
|
|
86
|
+
const storage = options.assessment
|
|
87
|
+
? undefined
|
|
88
|
+
: createStorage(element.dataset["storage"] ?? "explainer", { type: "sessionStorage" });
|
|
89
|
+
const key = `antwort:${element.ownerDocument.location.pathname}#${id}`;
|
|
90
|
+
const kept = storage?.load(key, isKept);
|
|
91
|
+
if (kept !== undefined && data.answer.includes(kept.way)) {
|
|
92
|
+
({ way, text, compared } = kept);
|
|
93
|
+
ticked = kept.ticked.filter((index) => index < points.length);
|
|
94
|
+
// a photo is never kept: comparing it again needs a new one
|
|
95
|
+
if (way === "photo") compared = false;
|
|
96
|
+
}
|
|
97
|
+
const keep = () => storage?.save(key, { way, text, ticked, compared } satisfies Kept);
|
|
98
|
+
|
|
99
|
+
const choice = both
|
|
100
|
+
? [
|
|
101
|
+
`<fieldset class="explainer-open-choice"><legend>${t("openTaskChoice")}</legend>`,
|
|
102
|
+
`<label><input type="radio" name="${id}-way" value="text" checked> <span>${t("openTaskType")}</span></label>`,
|
|
103
|
+
`<label><input type="radio" name="${id}-way" value="photo"> <span>${t("openTaskPaper")}</span></label>`,
|
|
104
|
+
`</fieldset>`,
|
|
105
|
+
].join("")
|
|
106
|
+
: "";
|
|
107
|
+
live.innerHTML = [
|
|
108
|
+
`<div class="explainer-open-write">`,
|
|
109
|
+
choice,
|
|
110
|
+
`<div class="explainer-open-text"><label for="${id}-text">${t("openTaskAnswer")}</label>`,
|
|
111
|
+
`<textarea id="${id}-text" rows="6"></textarea></div>`,
|
|
112
|
+
`<div class="explainer-open-photo" hidden><p>${t("openTaskPaperHow")}</p>`,
|
|
113
|
+
`<label class="explainer-open-shoot"><input type="file" accept="image/*" capture="environment"> <span>${t("openTaskPhoto")}</span></label>`,
|
|
114
|
+
`<img class="explainer-open-preview" alt="${t("openTaskPhotoAlt")}" hidden>`,
|
|
115
|
+
`<p class="explainer-open-local"><small>${t("openTaskPhotoLocal")}</small></p></div>`,
|
|
116
|
+
`<p class="explainer-open-need" aria-live="polite"></p>`,
|
|
117
|
+
`<p class="explainer-open-locked"><small>${t("openTaskLocked")}</small></p>`,
|
|
118
|
+
`</div>`,
|
|
119
|
+
`<fieldset class="explainer-open-check" hidden><legend>${t("openTaskCheck")}</legend>`,
|
|
120
|
+
`<p>${t("openTaskCheckHow")}</p>`,
|
|
121
|
+
points
|
|
122
|
+
.map(
|
|
123
|
+
(point, index) =>
|
|
124
|
+
`<label><input type="checkbox" value="${index}"> <span>${escape(point.text)}${point.kind === "form" ? ` <small>(${t("openTaskForm")})</small>` : ""}${point.href === undefined ? "" : ` <a href="${escape(point.href)}">${t("openTaskSource")}</a>`}</span></label>`,
|
|
125
|
+
)
|
|
126
|
+
.join(""),
|
|
127
|
+
`</fieldset>`,
|
|
128
|
+
`<p class="question-actions">`,
|
|
129
|
+
`<button type="button" class="explainer-open-compare">${t("openTaskCompare")}</button>`,
|
|
130
|
+
` <button type="button" class="explainer-open-change" hidden>${t("openTaskChange")}</button>`,
|
|
131
|
+
`</p>`,
|
|
132
|
+
].join("");
|
|
133
|
+
live.hidden = false;
|
|
134
|
+
// the tutor's help button goes into the row of "Vergleichen" (explainer.ts)
|
|
135
|
+
element.dispatchEvent(new CustomEvent(SHOWN_EVENT, { bubbles: true }));
|
|
136
|
+
|
|
137
|
+
const part = <T extends HTMLElement>(selector: string) => live.querySelector<T>(selector);
|
|
138
|
+
const write = part<HTMLElement>(".explainer-open-write");
|
|
139
|
+
const textPart = part<HTMLElement>(".explainer-open-text");
|
|
140
|
+
const photoPart = part<HTMLElement>(".explainer-open-photo");
|
|
141
|
+
const field = part<HTMLTextAreaElement>("textarea");
|
|
142
|
+
const file = part<HTMLInputElement>('input[type="file"]');
|
|
143
|
+
const preview = part<HTMLImageElement>(".explainer-open-preview");
|
|
144
|
+
const shoot = part<HTMLElement>(".explainer-open-shoot span");
|
|
145
|
+
const need = part<HTMLElement>(".explainer-open-need");
|
|
146
|
+
const check = part<HTMLFieldSetElement>(".explainer-open-check");
|
|
147
|
+
const compare = part<HTMLButtonElement>(".explainer-open-compare");
|
|
148
|
+
const change = part<HTMLButtonElement>(".explainer-open-change");
|
|
149
|
+
|
|
150
|
+
const ready = () => (way === "text" ? text.trim() !== "" : photo !== undefined);
|
|
151
|
+
const current = (): OpenTaskResult => {
|
|
152
|
+
const answer = way === "text" ? { text: text.trim() } : {};
|
|
153
|
+
if (!compared) return { ...empty(), ...answer, photo: way === "photo" && photo !== undefined };
|
|
154
|
+
return { ...openTaskState(ticked, points.length), ticked: [...ticked], ...answer, photo: way === "photo" };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** The step the learner is at: writing, or comparing. */
|
|
158
|
+
const show = () => {
|
|
159
|
+
if (textPart) textPart.hidden = way !== "text";
|
|
160
|
+
if (photoPart) photoPart.hidden = way !== "photo";
|
|
161
|
+
if (write) write.hidden = compared;
|
|
162
|
+
if (check) check.hidden = !compared;
|
|
163
|
+
if (compare) compare.hidden = compared;
|
|
164
|
+
// the model answer opens with the comparing, after the learner's own answer
|
|
165
|
+
if (solution) solution.hidden = !compared;
|
|
166
|
+
if (change) change.hidden = !compared;
|
|
167
|
+
if (need)
|
|
168
|
+
need.textContent =
|
|
169
|
+
compared || ready() ? "" : message(lang, way === "text" ? "openTaskWriteFirst" : "openTaskPhotoFirst");
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
for (const radio of live.querySelectorAll<HTMLInputElement>(".explainer-open-choice input")) {
|
|
173
|
+
radio.addEventListener("change", () => {
|
|
174
|
+
if (radio.checked) way = radio.value === "photo" ? "photo" : "text";
|
|
175
|
+
keep();
|
|
176
|
+
show();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
field?.addEventListener("input", () => {
|
|
180
|
+
text = field.value;
|
|
181
|
+
keep();
|
|
182
|
+
show();
|
|
183
|
+
});
|
|
184
|
+
file?.addEventListener("change", () => {
|
|
185
|
+
const chosen = file.files?.[0];
|
|
186
|
+
if (!chosen) return;
|
|
187
|
+
// shown from memory only: never stored, never sent
|
|
188
|
+
if (photo !== undefined) URL.revokeObjectURL(photo);
|
|
189
|
+
photo = URL.createObjectURL(chosen);
|
|
190
|
+
if (preview) {
|
|
191
|
+
preview.src = photo;
|
|
192
|
+
preview.hidden = false;
|
|
193
|
+
}
|
|
194
|
+
if (shoot) shoot.textContent = message(lang, "openTaskPhotoAgain");
|
|
195
|
+
show();
|
|
196
|
+
});
|
|
197
|
+
compare?.addEventListener("click", () => {
|
|
198
|
+
if (!ready()) {
|
|
199
|
+
show();
|
|
200
|
+
if (need) moveFocus(need);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
compared = true;
|
|
204
|
+
keep();
|
|
205
|
+
show();
|
|
206
|
+
const legend = check?.querySelector<HTMLElement>("legend");
|
|
207
|
+
if (legend) moveFocus(legend);
|
|
208
|
+
options.onResult?.(current(), true);
|
|
209
|
+
});
|
|
210
|
+
change?.addEventListener("click", () => {
|
|
211
|
+
compared = false;
|
|
212
|
+
keep();
|
|
213
|
+
show();
|
|
214
|
+
if (field && way === "text") moveFocus(field);
|
|
215
|
+
});
|
|
216
|
+
for (const box of check?.querySelectorAll<HTMLInputElement>("input") ?? []) {
|
|
217
|
+
box.addEventListener("change", () => {
|
|
218
|
+
const index = Number(box.value);
|
|
219
|
+
ticked = box.checked ? [...ticked, index].sort((a, b) => a - b) : ticked.filter((one) => one !== index);
|
|
220
|
+
keep();
|
|
221
|
+
options.onResult?.(current(), false);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// what the tab kept: the way, the text and the ticks
|
|
225
|
+
for (const radio of live.querySelectorAll<HTMLInputElement>(".explainer-open-choice input"))
|
|
226
|
+
radio.checked = radio.value === way;
|
|
227
|
+
if (field) field.value = text;
|
|
228
|
+
for (const box of check?.querySelectorAll<HTMLInputElement>("input") ?? [])
|
|
229
|
+
box.checked = ticked.includes(Number(box.value));
|
|
230
|
+
show();
|
|
231
|
+
return { result: current };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The names of the points at `indexes` (each from 0): P1, P2, … */
|
|
235
|
+
export const pointNames = (indexes: readonly number[]): string => indexes.map((index) => `P${index + 1}`).join(", ");
|