@octolearn/training-ui 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +413 -3
- package/THIRD-PARTY-NOTICES.md +211 -0
- package/dist/chunks/core-rJUDTpGS.js +592 -0
- package/dist/chunks/define-elements-CeSpZJXj.js +6111 -0
- package/dist/chunks/font-data-DlFp9eSH.js +8 -0
- package/dist/core.js +11 -0
- package/dist/define.js +52 -0
- package/dist/index.js +50 -0
- package/dist/octolearn-training-ui.bundle.js +6441 -0
- package/dist/types/base-element.d.ts +35 -0
- package/dist/types/content.d.ts +51 -0
- package/dist/types/core.d.ts +13 -0
- package/dist/types/d-icons.d.ts +10 -0
- package/dist/types/define-elements.d.ts +18 -0
- package/dist/types/define.d.ts +1 -0
- package/dist/types/diff.d.ts +34 -0
- package/dist/types/elements/arithmetic-question.d.ts +31 -0
- package/dist/types/elements/card.d.ts +43 -0
- package/dist/types/elements/choice-pair.d.ts +21 -0
- package/dist/types/elements/dictation-question.d.ts +51 -0
- package/dist/types/elements/feedback-toast.d.ts +29 -0
- package/dist/types/elements/feedback.d.ts +87 -0
- package/dist/types/elements/flashcard.d.ts +46 -0
- package/dist/types/elements/matching-round.d.ts +61 -0
- package/dist/types/elements/multiple-choice.d.ts +33 -0
- package/dist/types/elements/picture-question.d.ts +30 -0
- package/dist/types/elements/progress.d.ts +24 -0
- package/dist/types/elements/question-answer.d.ts +20 -0
- package/dist/types/elements/question-parts.d.ts +88 -0
- package/dist/types/elements/question.d.ts +37 -0
- package/dist/types/elements/text-answer.d.ts +57 -0
- package/dist/types/elements/tile-answer.d.ts +43 -0
- package/dist/types/elements/today-stats.d.ts +17 -0
- package/dist/types/elements/trainer-overlay.d.ts +45 -0
- package/dist/types/elements/trainer-panel.d.ts +46 -0
- package/dist/types/elements/trainer.d.ts +57 -0
- package/dist/types/elements/true-false.d.ts +37 -0
- package/dist/types/events.d.ts +53 -0
- package/dist/types/feedback-message.d.ts +34 -0
- package/dist/types/font-data.d.ts +2 -0
- package/dist/types/fonts.d.ts +9 -0
- package/dist/types/i18n/strings.d.ts +233 -0
- package/dist/types/icons.d.ts +21 -0
- package/dist/types/images.d.ts +2 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/keys.d.ts +6 -0
- package/dist/types/session/third-party-transport.d.ts +22 -0
- package/dist/types/session/training-session.d.ts +167 -0
- package/dist/types/session/transport.d.ts +44 -0
- package/dist/types/speech.d.ts +71 -0
- package/dist/types/styles/base.d.ts +9 -0
- package/dist/types/styles/d-parts.d.ts +13 -0
- package/dist/types/styles/tokens.d.ts +95 -0
- package/dist/types/viewport.d.ts +35 -0
- package/package.json +67 -6
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every text the elements show or announce. The German and English texts are the ones of the octolearn app (keys in
|
|
3
|
+
* octolearn's i18n files: train-learning-content.*, and a11y.* for the few marked "global"). Placeholders like {word} are
|
|
4
|
+
* filled in by format().
|
|
5
|
+
*/
|
|
6
|
+
export interface TrainingStrings {
|
|
7
|
+
check: string;
|
|
8
|
+
statistic: string;
|
|
9
|
+
correct: string;
|
|
10
|
+
wrong: string;
|
|
11
|
+
learningContentFolder: string;
|
|
12
|
+
practiceBadge: string;
|
|
13
|
+
settings: string;
|
|
14
|
+
repeat: string;
|
|
15
|
+
stop: string;
|
|
16
|
+
playHintMessage: string;
|
|
17
|
+
playHintButton: string;
|
|
18
|
+
/** {language} */
|
|
19
|
+
noVoiceMessage: string;
|
|
20
|
+
feedbackCorrect: string;
|
|
21
|
+
feedbackWrong: string;
|
|
22
|
+
yourAnswer: string;
|
|
23
|
+
correctAnswer: string;
|
|
24
|
+
typoAccepted: string;
|
|
25
|
+
promptTranslate: string;
|
|
26
|
+
promptPicture: string;
|
|
27
|
+
placeholderTranslation: string;
|
|
28
|
+
placeholderAnswer: string;
|
|
29
|
+
placeholderPictureVocabulary: string;
|
|
30
|
+
placeholderPictureRiddle: string;
|
|
31
|
+
placeholderNumber: string;
|
|
32
|
+
placeholderDictation: string;
|
|
33
|
+
dictationHeading: string;
|
|
34
|
+
audioControls: string;
|
|
35
|
+
answerHeading: string;
|
|
36
|
+
vocabularyHeading: string;
|
|
37
|
+
questionHeading: string;
|
|
38
|
+
instructions: string;
|
|
39
|
+
questionText: string;
|
|
40
|
+
questionTextPlain: string;
|
|
41
|
+
/** {word} */
|
|
42
|
+
wordToTranslate: string;
|
|
43
|
+
/** {question} */
|
|
44
|
+
questionToAnswer: string;
|
|
45
|
+
/** {word} */
|
|
46
|
+
enterTranslation: string;
|
|
47
|
+
/** {question} */
|
|
48
|
+
enterAnswer: string;
|
|
49
|
+
/** {word} */
|
|
50
|
+
chooseTranslation: string;
|
|
51
|
+
/** {question} */
|
|
52
|
+
chooseAnswer: string;
|
|
53
|
+
/** global a11y.choose-answer: for questions without a text (picture, dictation, arithmetic) */
|
|
54
|
+
chooseAnswerPlain: string;
|
|
55
|
+
/** global a11y.enter-dictation */
|
|
56
|
+
enterDictation: string;
|
|
57
|
+
/** global a11y.math-task */
|
|
58
|
+
mathTask: string;
|
|
59
|
+
pictureHeading: string;
|
|
60
|
+
pictureQuestion: string;
|
|
61
|
+
pictureAlt: string;
|
|
62
|
+
pictureCaptionVocabulary: string;
|
|
63
|
+
pictureCaptionRiddle: string;
|
|
64
|
+
/** the button on the picture that shows it large */
|
|
65
|
+
enlargePicture: string;
|
|
66
|
+
closePicture: string;
|
|
67
|
+
enterPictureVocabulary: string;
|
|
68
|
+
enterPictureRiddle: string;
|
|
69
|
+
arithmeticHeading: string;
|
|
70
|
+
enterNumber: string;
|
|
71
|
+
equals: string;
|
|
72
|
+
unknownResult: string;
|
|
73
|
+
/** {first} {operator} {second} */
|
|
74
|
+
expression: string;
|
|
75
|
+
operatorPlus: string;
|
|
76
|
+
operatorMinus: string;
|
|
77
|
+
operatorTimes: string;
|
|
78
|
+
operatorDivided: string;
|
|
79
|
+
/** {current} {total}: the stars of design D */
|
|
80
|
+
progress: string;
|
|
81
|
+
progressRight: string;
|
|
82
|
+
progressWrong: string;
|
|
83
|
+
/** Design D */
|
|
84
|
+
done: string;
|
|
85
|
+
dontKnow: string;
|
|
86
|
+
/** {key} is the key, e.g. Enter */
|
|
87
|
+
pressKeyOr: string;
|
|
88
|
+
/** {last}: the highest number key */
|
|
89
|
+
tapOrPressNumber: string;
|
|
90
|
+
promptDictation: string;
|
|
91
|
+
promptDictationChoice: string;
|
|
92
|
+
listenWord: string;
|
|
93
|
+
listenSlowly: string;
|
|
94
|
+
readAloud: string;
|
|
95
|
+
readWordAloud: string;
|
|
96
|
+
readQuestionAloud: string;
|
|
97
|
+
/** {language} */
|
|
98
|
+
promptVocabularyTo: string;
|
|
99
|
+
/** {language} */
|
|
100
|
+
answerIn: string;
|
|
101
|
+
promptArithmetic: string;
|
|
102
|
+
writeWord: string;
|
|
103
|
+
yourWord: string;
|
|
104
|
+
result: string;
|
|
105
|
+
yourAnswerField: string;
|
|
106
|
+
labelDictation: string;
|
|
107
|
+
labelVocabulary: string;
|
|
108
|
+
labelQuestion: string;
|
|
109
|
+
labelChoice: string;
|
|
110
|
+
labelArithmetic: string;
|
|
111
|
+
labelPicture: string;
|
|
112
|
+
today: string;
|
|
113
|
+
/** Design D feedback */
|
|
114
|
+
feedbackRight: string;
|
|
115
|
+
feedbackNotQuite: string;
|
|
116
|
+
/** several sentences separated by | ; one is picked per answer */
|
|
117
|
+
praise: string;
|
|
118
|
+
praiseListening: string;
|
|
119
|
+
/** {n} */
|
|
120
|
+
reasonMissingOne: string;
|
|
121
|
+
reasonMissingMany: string;
|
|
122
|
+
reasonExtraOne: string;
|
|
123
|
+
reasonExtraMany: string;
|
|
124
|
+
reasonOther: string;
|
|
125
|
+
reasonDontKnow: string;
|
|
126
|
+
reasonChoice: string;
|
|
127
|
+
reasonUnchecked: string;
|
|
128
|
+
solutionLabel: string;
|
|
129
|
+
youWrote: string;
|
|
130
|
+
comeBackWord: string;
|
|
131
|
+
comeBack: string;
|
|
132
|
+
next: string;
|
|
133
|
+
pointsOne: string;
|
|
134
|
+
/** {n} */
|
|
135
|
+
pointsMany: string;
|
|
136
|
+
/** {n} */
|
|
137
|
+
streak: string;
|
|
138
|
+
listenAgain: string;
|
|
139
|
+
yourChoice: string;
|
|
140
|
+
/** Tiles and Wortfragment (OC-121…126) */
|
|
141
|
+
promptLetterSalad: string;
|
|
142
|
+
promptWordSalad: string;
|
|
143
|
+
instructionListenLetters: string;
|
|
144
|
+
instructionListenWords: string;
|
|
145
|
+
instructionLetters: string;
|
|
146
|
+
instructionWords: string;
|
|
147
|
+
promptFragment: string;
|
|
148
|
+
/** {length} {first} {last} */
|
|
149
|
+
fragmentHint: string;
|
|
150
|
+
tilesBank: string;
|
|
151
|
+
/** {answer} {left} */
|
|
152
|
+
tilesSlotsLetters: string;
|
|
153
|
+
/** {answer} {left} */
|
|
154
|
+
tilesSlotsWords: string;
|
|
155
|
+
/** {tile} */
|
|
156
|
+
tilesPlace: string;
|
|
157
|
+
/** {tile} */
|
|
158
|
+
tilesRemove: string;
|
|
159
|
+
tilesReset: string;
|
|
160
|
+
/** {answer} */
|
|
161
|
+
tilesSoFar: string;
|
|
162
|
+
tilesKeyboardHint: string;
|
|
163
|
+
labelLetterSalad: string;
|
|
164
|
+
labelWordSalad: string;
|
|
165
|
+
labelFragment: string;
|
|
166
|
+
promptTrueFalse: string;
|
|
167
|
+
instructionTrueFalse: string;
|
|
168
|
+
trueFalseTrue: string;
|
|
169
|
+
trueFalseFalse: string;
|
|
170
|
+
/** "Antippen oder Pfeiltaste links/rechts drücken." */
|
|
171
|
+
arrowKeysHint: string;
|
|
172
|
+
labelTrueFalse: string;
|
|
173
|
+
reasonTrueFalseYes: string;
|
|
174
|
+
reasonTrueFalseNo: string;
|
|
175
|
+
/** Before what the learner tapped: "Deine Wahl:" */
|
|
176
|
+
youChose: string;
|
|
177
|
+
instructionFlashcard: string;
|
|
178
|
+
flashcardReveal: string;
|
|
179
|
+
flashcardFailed: string;
|
|
180
|
+
flashcardAnswer: string;
|
|
181
|
+
flashcardRate: string;
|
|
182
|
+
flashcardKnown: string;
|
|
183
|
+
flashcardUnknown: string;
|
|
184
|
+
labelFlashcard: string;
|
|
185
|
+
reasonFlashcard: string;
|
|
186
|
+
labelMatching: string;
|
|
187
|
+
promptMatching: string;
|
|
188
|
+
instructionMatching: string;
|
|
189
|
+
matchingQuestions: string;
|
|
190
|
+
matchingAnswers: string;
|
|
191
|
+
/** {found} {total} */
|
|
192
|
+
matchingFound: string;
|
|
193
|
+
/** {question} {answer} */
|
|
194
|
+
matchingRight: string;
|
|
195
|
+
/** {question} {answer} */
|
|
196
|
+
matchingWrong: string;
|
|
197
|
+
matchingFailed: string;
|
|
198
|
+
/** {correct} {total} */
|
|
199
|
+
matchingSummary: string;
|
|
200
|
+
/** {n} */
|
|
201
|
+
matchingPicture: string;
|
|
202
|
+
/** {answer}: the right answer under a wrongly paired question */
|
|
203
|
+
matchingSolution: string;
|
|
204
|
+
/** Name of the trainer, the overlay dialog and the side panel: "Lernfrage" */
|
|
205
|
+
trainerLabel: string;
|
|
206
|
+
loadingQuestions: string;
|
|
207
|
+
loadFailed: string;
|
|
208
|
+
retry: string;
|
|
209
|
+
dueDone: string;
|
|
210
|
+
practiseOn: string;
|
|
211
|
+
allDone: string;
|
|
212
|
+
gamePaused: string;
|
|
213
|
+
roundLeftOne: string;
|
|
214
|
+
/** {n} */
|
|
215
|
+
roundLeftMany: string;
|
|
216
|
+
roundDone: string;
|
|
217
|
+
/** {n} */
|
|
218
|
+
starsToday: string;
|
|
219
|
+
}
|
|
220
|
+
export declare const de: TrainingStrings;
|
|
221
|
+
export declare const en: TrainingStrings;
|
|
222
|
+
export type TrainingLanguage = 'de' | 'en';
|
|
223
|
+
export declare const DEFAULT_LANGUAGE: TrainingLanguage;
|
|
224
|
+
/** "en-GB" → "en"; anything without texts falls back to German, octolearn's own language. */
|
|
225
|
+
export declare function toTrainingLanguage(lang: string | null | undefined): TrainingLanguage;
|
|
226
|
+
export declare function stringsFor(lang: TrainingLanguage, overrides?: Partial<TrainingStrings>): TrainingStrings;
|
|
227
|
+
/** Fills {name} placeholders. Values are inserted as plain text; the elements never render texts as HTML. */
|
|
228
|
+
export declare function format(template: string, params: Record<string, string | number | null | undefined>): string;
|
|
229
|
+
/**
|
|
230
|
+
* The language of an element: its own `lang` attribute, else the nearest ancestor's, also across shadow roots (an element
|
|
231
|
+
* inside another element's shadow root), else <html lang>, else German.
|
|
232
|
+
*/
|
|
233
|
+
export declare function resolveLanguage(element: Element): TrainingLanguage;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { type TemplateResult } from 'lit';
|
|
3
|
+
/**
|
|
4
|
+
* The Font Awesome Free icons octolearn's training page uses (Font Awesome Free by Fonticons, Inc., icons under
|
|
5
|
+
* CC BY 4.0, see THIRD-PARTY-NOTICES.md). Only these end up in the build.
|
|
6
|
+
*/
|
|
7
|
+
declare const icons: {
|
|
8
|
+
check: IconDefinition;
|
|
9
|
+
divide: IconDefinition;
|
|
10
|
+
dumbbell: IconDefinition;
|
|
11
|
+
gear: IconDefinition;
|
|
12
|
+
headphones: IconDefinition;
|
|
13
|
+
minus: IconDefinition;
|
|
14
|
+
play: IconDefinition;
|
|
15
|
+
plus: IconDefinition;
|
|
16
|
+
xmark: IconDefinition;
|
|
17
|
+
};
|
|
18
|
+
export type IconName = keyof typeof icons;
|
|
19
|
+
/** A decorative icon (hidden from screen readers), 1em high like Font Awesome's svg-inline--fa. */
|
|
20
|
+
export declare function icon(name: IconName): TemplateResult;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type { FrontendContent, LearningMode, MatchingRound, MatchingRoundItem, QueryFormat } from './content';
|
|
2
|
+
export { defineTrainingElements, trainingElements, type DefineTrainingElementsOptions } from './define-elements';
|
|
3
|
+
export { OctolearnCard, type CardState } from './elements/card';
|
|
4
|
+
export { OctolearnArithmeticQuestion, parseArithmeticTask, type ArithmeticTask } from './elements/arithmetic-question';
|
|
5
|
+
export { OctolearnDictationQuestion } from './elements/dictation-question';
|
|
6
|
+
export { AUTO_CONTINUE_MS, OctolearnFeedback, type FeedbackMessage, type FeedbackResult } from './elements/feedback';
|
|
7
|
+
export { OctolearnFeedbackToast } from './elements/feedback-toast';
|
|
8
|
+
export { KNOWN_ANSWER, NOT_KNOWN_ANSWER, OctolearnFlashcard, type FlashcardRevealer } from './elements/flashcard';
|
|
9
|
+
export { OctolearnMatchingRound, type PairChecker } from './elements/matching-round';
|
|
10
|
+
export { OctolearnMultipleChoice } from './elements/multiple-choice';
|
|
11
|
+
export { OctolearnPictureQuestion } from './elements/picture-question';
|
|
12
|
+
export { OctolearnProgress, type QuestionResult } from './elements/progress';
|
|
13
|
+
export { OctolearnQuestion } from './elements/question';
|
|
14
|
+
export { OctolearnQuestionAnswer } from './elements/question-answer';
|
|
15
|
+
export { describeMatchingRound, describeQuestion, languageName } from './elements/question-parts';
|
|
16
|
+
export { OctolearnTextAnswer } from './elements/text-answer';
|
|
17
|
+
export { OctolearnTileAnswer } from './elements/tile-answer';
|
|
18
|
+
export { OctolearnTodayStats } from './elements/today-stats';
|
|
19
|
+
export { OctolearnTrainer } from './elements/trainer';
|
|
20
|
+
export { OctolearnTrainerOverlay } from './elements/trainer-overlay';
|
|
21
|
+
export { OctolearnTrainerPanel } from './elements/trainer-panel';
|
|
22
|
+
export { FALSE_ANSWER, OctolearnTrueFalse, TRUE_ANSWER } from './elements/true-false';
|
|
23
|
+
export { DONT_KNOW_ANSWER } from './events';
|
|
24
|
+
export { letterDiff, type DiffOp, type LetterDiff } from './diff';
|
|
25
|
+
export { ANSWER_EVENT, type AnswerEvent, type AnswerEventDetail, type PairCheckedDetail, type SpeakEvent, type SpeakEventDetail, } from './events';
|
|
26
|
+
export { loadFonts } from './fonts';
|
|
27
|
+
export { de, en, type TrainingLanguage, type TrainingStrings } from './i18n/strings';
|
|
28
|
+
export { createThirdPartyTransport, type ThirdPartyTransportOptions } from './session/third-party-transport';
|
|
29
|
+
export { TrainingSession, type AnswerOutcome, type PairOutcome, type RoundProgress, type SessionState, type TrainingSessionOptions, } from './session/training-session';
|
|
30
|
+
export { TransportError, type AnswerEvaluation, type TodaysStatistic, type Transport } from './session/transport';
|
|
31
|
+
export { getSpeechController, MAX_RATE, MIN_RATE, SpeechController, type SpeakOptions, type SpeakResult, type SpeechControllerOptions, } from './speech';
|
|
32
|
+
export { getViewportWatcher, SHORT_VIEWPORT_PX, ViewportWatcher } from './viewport';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a key pressed anywhere on the page may be taken by an answer element (number keys for the tiles, arrow keys for
|
|
3
|
+
* Stimmt/Stimmt nicht): not with a modifier, not when another handler already used it, not while the learner types in a
|
|
4
|
+
* field, and not while the element is hidden.
|
|
5
|
+
*/
|
|
6
|
+
export declare function documentKeyFree(event: KeyboardEvent, host: Element): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LearningMode, QueryFormat } from '../content';
|
|
2
|
+
import { type Transport } from './transport';
|
|
3
|
+
export interface ThirdPartyTransportOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Where octolearn's `/api` is reached, usually **your own server**, which forwards to octolearn and adds the `apiKey` and
|
|
6
|
+
* `apiSecret` headers. The secret must never reach the browser. Example: `'/octolearn'` makes the transport call
|
|
7
|
+
* `/octolearn/third-party/next-relevant-questions`.
|
|
8
|
+
*/
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/** Headers for every request, typically the learner's token from the device flow: `{ Authorization: 'Bearer …' }`. */
|
|
11
|
+
headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
12
|
+
/** Questions per request (default 5). */
|
|
13
|
+
batchSize?: number;
|
|
14
|
+
/** Only these answer formats (default: all the API sends to games). */
|
|
15
|
+
queryFormats?: readonly QueryFormat[];
|
|
16
|
+
/** Only these learning modes. */
|
|
17
|
+
learningModes?: readonly LearningMode[];
|
|
18
|
+
/** Default: the global fetch. */
|
|
19
|
+
fetch?: (url: string, init: RequestInit) => Promise<Response>;
|
|
20
|
+
}
|
|
21
|
+
/** A Transport for octolearn's third-party API (see the third-party integration guide). */
|
|
22
|
+
export declare function createThirdPartyTransport(options: ThirdPartyTransportOptions): Transport;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { FrontendContent, MatchingRound, MatchingRoundItem } from '../content';
|
|
2
|
+
import type { FeedbackMessage, QuestionResult } from '../feedback-message';
|
|
3
|
+
import type { AnswerEvaluation, Transport } from './transport';
|
|
4
|
+
/**
|
|
5
|
+
* - `idle`: start() not called yet
|
|
6
|
+
* - `loading`: waiting for questions
|
|
7
|
+
* - `question`: `current` is the question to show
|
|
8
|
+
* - `due-done`: everything due today is answered; what is left is practice that changes no level. Offer to go on with
|
|
9
|
+
* startPractising() (octolearn OC-225).
|
|
10
|
+
* - `done`: nothing left today
|
|
11
|
+
* - `error`: the questions could not be loaded; reload() tries again
|
|
12
|
+
* - `feedback`: only with `holdForFeedback`: an answer was evaluated, `feedback` describes it; continue() goes on
|
|
13
|
+
* - `pairs`: a round of "Paare finden" (`round`) instead of a single question; check each pair with checkPair(), then
|
|
14
|
+
* finishRound() goes on. Only with a transport that offers matchingRound() (octolearn's web app).
|
|
15
|
+
*/
|
|
16
|
+
export type SessionState = 'idle' | 'loading' | 'question' | 'feedback' | 'pairs' | 'due-done' | 'done' | 'error';
|
|
17
|
+
/** Where the learner is in the current round of questions (design D's stars, "Frage 3 von 5"). */
|
|
18
|
+
export interface RoundProgress {
|
|
19
|
+
/** 1-based number of the question being asked or just answered. */
|
|
20
|
+
current: number;
|
|
21
|
+
total: number;
|
|
22
|
+
results: readonly QuestionResult[];
|
|
23
|
+
}
|
|
24
|
+
export interface AnswerOutcome {
|
|
25
|
+
content: FrontendContent;
|
|
26
|
+
answer: string;
|
|
27
|
+
/** null when the answer could not be sent; it then counts as wrong, like on octolearn's training page. */
|
|
28
|
+
evaluation: AnswerEvaluation | null;
|
|
29
|
+
correct: boolean;
|
|
30
|
+
/** Ready for `<octolearn-feedback>`.message or `<octolearn-feedback-toast>`.show(). */
|
|
31
|
+
feedback: FeedbackMessage;
|
|
32
|
+
}
|
|
33
|
+
/** One pair of a round, checked by the server (the session's `pair-checked` event). */
|
|
34
|
+
export interface PairOutcome {
|
|
35
|
+
item: MatchingRoundItem;
|
|
36
|
+
answer: string;
|
|
37
|
+
evaluation: AnswerEvaluation;
|
|
38
|
+
correct: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface TrainingSessionOptions {
|
|
41
|
+
/** Ask for the next questions when this many are left (default 2), so the next one is there without waiting. */
|
|
42
|
+
prefetchAt?: number;
|
|
43
|
+
/** How many answered questions are kept back from the very next turn (default 3). */
|
|
44
|
+
recentMemory?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Stay in state `feedback` after each answer until continue() is called, so the page can show the feedback in the card
|
|
47
|
+
* (design D). Default false: the next question is `current` right away.
|
|
48
|
+
*/
|
|
49
|
+
holdForFeedback?: boolean;
|
|
50
|
+
/** Questions per round, for the stars (default 5). */
|
|
51
|
+
roundSize?: number;
|
|
52
|
+
/**
|
|
53
|
+
* With a transport that offers matchingRound(): ask for a round of "Paare finden" at the start and after this many
|
|
54
|
+
* single answers (default 8, like octolearn's training page).
|
|
55
|
+
*/
|
|
56
|
+
matchingEvery?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The train loop without UI: loads questions, sends answers, keeps today's counts. The order rules are octolearn's
|
|
60
|
+
* training page's:
|
|
61
|
+
* - the next questions are asked for before the last one is answered (prefetch);
|
|
62
|
+
* - a refill drops questions that are still open on screen, which the server can't know about (OC-186);
|
|
63
|
+
* - the questions answered a moment ago go behind the others of a batch, so none comes twice in a row (OC-224), but are
|
|
64
|
+
* not dropped: a wrongly answered question comes back in the same session on purpose (OC-251);
|
|
65
|
+
* - with a transport that offers matchingRound(), a round of "Paare finden" comes at the start and after every 8 single
|
|
66
|
+
* answers (OC-125); its questions are dropped from the queue afterwards.
|
|
67
|
+
*
|
|
68
|
+
* Listen to `change` to redraw, to `answered` (detail: AnswerOutcome) for the feedback and to `pair-checked` (detail:
|
|
69
|
+
* PairOutcome) for the pairs of a round.
|
|
70
|
+
*
|
|
71
|
+
* ```js
|
|
72
|
+
* const session = new TrainingSession(createThirdPartyTransport({ baseUrl: '/octolearn', headers }));
|
|
73
|
+
* session.addEventListener('change', () => { question.content = session.current; stats.correct = session.correct; });
|
|
74
|
+
* question.addEventListener('octolearn-answer', async e => toast.show((await session.answer(e.detail.answer))!.feedback));
|
|
75
|
+
* await session.start();
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare class TrainingSession extends EventTarget {
|
|
79
|
+
private readonly transport;
|
|
80
|
+
/** Today's correct answers. */
|
|
81
|
+
correct: number;
|
|
82
|
+
/** Today's wrong answers (also those that could not be sent). */
|
|
83
|
+
wrong: number;
|
|
84
|
+
/** The learner chose to go on with practice after everything due was done. */
|
|
85
|
+
practising: boolean;
|
|
86
|
+
/** Right answers in a row in this session. */
|
|
87
|
+
streak: number;
|
|
88
|
+
/** The answer being shown with `holdForFeedback`, until continue(). */
|
|
89
|
+
feedback: AnswerOutcome | null;
|
|
90
|
+
/** The round of "Paare finden" to show (state `pairs`), until finishRound(). */
|
|
91
|
+
round: MatchingRound | null;
|
|
92
|
+
/** A round that arrived while a question was open; it comes after that question. */
|
|
93
|
+
private nextRound;
|
|
94
|
+
private roundRequested;
|
|
95
|
+
private answersSinceRound;
|
|
96
|
+
private roundResults;
|
|
97
|
+
private queue;
|
|
98
|
+
/** The next batch, fetched ahead; [] = the server has nothing more; null = none fetched. */
|
|
99
|
+
private buffer;
|
|
100
|
+
private started;
|
|
101
|
+
private loading;
|
|
102
|
+
private loadFailed;
|
|
103
|
+
private prefetching;
|
|
104
|
+
private prefetchFailed;
|
|
105
|
+
private finished;
|
|
106
|
+
private submitting;
|
|
107
|
+
private recentlyAnswered;
|
|
108
|
+
private readonly prefetchAt;
|
|
109
|
+
private readonly recentMemory;
|
|
110
|
+
private readonly holdForFeedback;
|
|
111
|
+
private readonly roundSize;
|
|
112
|
+
private readonly matchingEvery;
|
|
113
|
+
constructor(transport: Transport, options?: TrainingSessionOptions);
|
|
114
|
+
get state(): SessionState;
|
|
115
|
+
/** The question to show. */
|
|
116
|
+
get current(): FrontendContent | null;
|
|
117
|
+
/** The questions waiting, the current one first. */
|
|
118
|
+
get upcoming(): readonly FrontendContent[];
|
|
119
|
+
/** Only practice is left (nothing due) and the learner hasn't chosen to go on. */
|
|
120
|
+
get dueDone(): boolean;
|
|
121
|
+
/** The stars: the current question of the round and the results so far. A new round starts after `roundSize` answers. */
|
|
122
|
+
get roundProgress(): RoundProgress;
|
|
123
|
+
/** An answer is being sent; further answers are ignored until it is evaluated. */
|
|
124
|
+
get busy(): boolean;
|
|
125
|
+
/** Loads the first questions and today's counts. */
|
|
126
|
+
start(): Promise<void>;
|
|
127
|
+
/** Asks for questions again, e.g. after an error ("Erneut versuchen"). */
|
|
128
|
+
reload(): Promise<void>;
|
|
129
|
+
startPractising(): void;
|
|
130
|
+
/** With `holdForFeedback`: leaves the feedback and shows the next question ("Weiter"). */
|
|
131
|
+
continue(): void;
|
|
132
|
+
/**
|
|
133
|
+
* Sends the answer to the current question and moves on to the next one (with `holdForFeedback`: to the feedback).
|
|
134
|
+
* Returns null when there is no question, the answer is blank or another answer is still being sent. Pass
|
|
135
|
+
* `{ dontKnow: true }` for "Weiß ich nicht" (the `octolearn-answer` event's detail).
|
|
136
|
+
*/
|
|
137
|
+
answer(text: string, details?: {
|
|
138
|
+
dontKnow?: boolean;
|
|
139
|
+
}): Promise<AnswerOutcome | null>;
|
|
140
|
+
/**
|
|
141
|
+
* Checks one pair of the current round, like an answer to that question; counts it and fires `pair-checked`. Null when
|
|
142
|
+
* the pair is not part of the round or could not be checked (the pair then stays open).
|
|
143
|
+
*/
|
|
144
|
+
checkPair(item: MatchingRoundItem, answer: string): Promise<AnswerEvaluation | null>;
|
|
145
|
+
/** After a round ("Weiter"): the single questions go on; the ones the round just asked are dropped from the queue. */
|
|
146
|
+
finishRound(): void;
|
|
147
|
+
/** The answer of a Karteikarte (for `<octolearn-flashcard>`.reveal); rejects when the transport can't. */
|
|
148
|
+
revealFlashcard(content?: FrontendContent | null): Promise<string>;
|
|
149
|
+
/**
|
|
150
|
+
* Asks for a round of "Paare finden" (OC-125). Before the first answer it is shown at once; later it waits until the
|
|
151
|
+
* question on screen is answered. A round without items, or an error, just means single questions go on.
|
|
152
|
+
*/
|
|
153
|
+
private requestRound;
|
|
154
|
+
private load;
|
|
155
|
+
private loadStatistic;
|
|
156
|
+
private advance;
|
|
157
|
+
private prefetch;
|
|
158
|
+
private drainBuffer;
|
|
159
|
+
private rememberAnswered;
|
|
160
|
+
/**
|
|
161
|
+
* Puts what was answered a moment ago behind the other questions of the batch (OC-224). It is not left out: a wrongly
|
|
162
|
+
* answered question comes back in the same session on purpose, and a batch that lost its only due question would say
|
|
163
|
+
* "done" while that question is still waiting (OC-251). A lone question may come back.
|
|
164
|
+
*/
|
|
165
|
+
private preferNotRecentlyAnswered;
|
|
166
|
+
private changed;
|
|
167
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { FrontendContent, MatchingRound } from '../content';
|
|
2
|
+
/** The server's verdict on an answer (octolearn's AnswerEvaluationResultDTO). */
|
|
3
|
+
export interface AnswerEvaluation {
|
|
4
|
+
isCorrect: boolean;
|
|
5
|
+
/** Correct apart from a small typo. */
|
|
6
|
+
acceptedWithTypo?: boolean;
|
|
7
|
+
/** The server's note on the answer, e.g. about the typo. */
|
|
8
|
+
feedbackMessage?: string | null;
|
|
9
|
+
/** The solution; sent only after an attempt, never with the question. */
|
|
10
|
+
correctAnswer?: string | null;
|
|
11
|
+
}
|
|
12
|
+
/** Today's answers (octolearn's AmountOfLearningContentPerDay). */
|
|
13
|
+
export interface TodaysStatistic {
|
|
14
|
+
correctAnswerCount?: number | null;
|
|
15
|
+
wrongAnswerCount?: number | null;
|
|
16
|
+
totalAnswerCount?: number | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* How a TrainingSession talks to a server. createThirdPartyTransport() implements it for octolearn's third-party API;
|
|
20
|
+
* implement it yourself for any other backend.
|
|
21
|
+
*/
|
|
22
|
+
export interface Transport {
|
|
23
|
+
/** The next questions, due ones first; an empty list when nothing is left today. */
|
|
24
|
+
nextQuestions(): Promise<FrontendContent[]>;
|
|
25
|
+
submitAnswer(content: FrontendContent, answer: string): Promise<AnswerEvaluation>;
|
|
26
|
+
/** Optional: the counts the session starts from. */
|
|
27
|
+
todaysStatistic?(): Promise<TodaysStatistic>;
|
|
28
|
+
/**
|
|
29
|
+
* Optional, octolearn's web app only: the answer of a Karteikarte (OC-128), asked for when the learner turns the card.
|
|
30
|
+
* The server hands it out only while the content is a Karteikarte.
|
|
31
|
+
*/
|
|
32
|
+
revealFlashcard?(content: FrontendContent): Promise<string>;
|
|
33
|
+
/** Optional, octolearn's web app only: a round of "Paare verbinden" (OC-125); one without items means none now. */
|
|
34
|
+
matchingRound?(): Promise<MatchingRound>;
|
|
35
|
+
}
|
|
36
|
+
/** A request the server answered with an error status. */
|
|
37
|
+
export declare class TransportError extends Error {
|
|
38
|
+
readonly status: number;
|
|
39
|
+
/** From X-Rate-Limit-Retry-After-Seconds on a 429: wait this long before asking again. */
|
|
40
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
41
|
+
constructor(status: number, message: string,
|
|
42
|
+
/** From X-Rate-Limit-Retry-After-Seconds on a 429: wait this long before asking again. */
|
|
43
|
+
retryAfterSeconds?: number | undefined);
|
|
44
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** The range of octolearn's speed slider. */
|
|
2
|
+
export declare const MIN_RATE = 0.5;
|
|
3
|
+
export declare const MAX_RATE = 1.5;
|
|
4
|
+
/** How long a word waits for the browser's voice list before it is spoken with the language alone. */
|
|
5
|
+
export declare const VOICE_WAIT_MS = 1000;
|
|
6
|
+
/**
|
|
7
|
+
* - `spoken`: speaking (or queued until the browser has loaded its voices)
|
|
8
|
+
* - `blocked`: the browser wants a user gesture first (autoplay policy); call playPending() from a click
|
|
9
|
+
* - `unavailable`: no speech synthesis in this browser, or nothing to say
|
|
10
|
+
*/
|
|
11
|
+
export type SpeakResult = 'spoken' | 'blocked' | 'unavailable';
|
|
12
|
+
export interface SpeakOptions {
|
|
13
|
+
/** Speed for this request only, e.g. 0.7 for "Langsam anhören"; default: the learner's speed. */
|
|
14
|
+
rate?: number;
|
|
15
|
+
/** Language of the text (e.g. "en" for a vocabulary word); default: the controller's `preferredLanguage`. */
|
|
16
|
+
lang?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SpeechControllerOptions {
|
|
19
|
+
/** Voice names tried first when the learner hasn't chosen one. Default: Google Deutsch. */
|
|
20
|
+
preferredVoices?: readonly string[];
|
|
21
|
+
/** Language of text spoken without a `lang` (default `de`); also the default voice of a settings dialog. */
|
|
22
|
+
preferredLanguage?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reads dictation words aloud with the Web Speech API. Ported from octolearn's training page: the voice and speed the
|
|
26
|
+
* learner chose are remembered, the newest request wins, and a browser that blocks speech until the first user gesture
|
|
27
|
+
* keeps the text until playPending() is called from one.
|
|
28
|
+
*
|
|
29
|
+
* A text is only read by a voice of its language: the learner's voice if it speaks that language, else a preferred
|
|
30
|
+
* voice or the first voice of it. Without one the browser gets only the language, never a voice of another language
|
|
31
|
+
* (left to itself, Chrome takes the voice of the browser's language, so German words came out English).
|
|
32
|
+
*/
|
|
33
|
+
export declare class SpeechController {
|
|
34
|
+
voices: SpeechSynthesisVoice[];
|
|
35
|
+
selectedVoice: SpeechSynthesisVoice | null;
|
|
36
|
+
rate: number;
|
|
37
|
+
private pending;
|
|
38
|
+
private requestId;
|
|
39
|
+
private connections;
|
|
40
|
+
private voiceWait;
|
|
41
|
+
private readonly preferredVoices;
|
|
42
|
+
private readonly preferredLanguage;
|
|
43
|
+
constructor(options?: SpeechControllerOptions);
|
|
44
|
+
static get supported(): boolean;
|
|
45
|
+
/** Starts listening for the browser's voice list. Call disconnect() as often as connect(). */
|
|
46
|
+
connect(): void;
|
|
47
|
+
disconnect(): void;
|
|
48
|
+
speak(input: string | null | undefined, options?: SpeakOptions): SpeakResult;
|
|
49
|
+
/** Speaks the text that was blocked; call it from a click. */
|
|
50
|
+
playPending(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the browser has a voice for the language (default: `preferredLanguage`); `undefined` while its voice list
|
|
53
|
+
* is still loading or without speech synthesis.
|
|
54
|
+
*/
|
|
55
|
+
hasVoiceFor(lang?: string): boolean | undefined;
|
|
56
|
+
stop(): void;
|
|
57
|
+
/** Remembers the voice (by name; the voice objects change when the browser reloads its list) and the speed. */
|
|
58
|
+
choose(voice: SpeechSynthesisVoice | null, rate?: number): void;
|
|
59
|
+
private readonly handleVoicesChanged;
|
|
60
|
+
/** Speaks the pending text now if the voice list is known, else on voiceschanged or after VOICE_WAIT_MS. */
|
|
61
|
+
private flushWhenVoicesKnown;
|
|
62
|
+
private stopWaitingForVoices;
|
|
63
|
+
/** Reads the browser's voice list; false while it is still empty. */
|
|
64
|
+
private refreshVoices;
|
|
65
|
+
/** A voice that speaks the language: the learner's, else a preferred one, else the first; never one of another language. */
|
|
66
|
+
private voiceFor;
|
|
67
|
+
private preferredVoice;
|
|
68
|
+
private flushPending;
|
|
69
|
+
}
|
|
70
|
+
/** The controller the dictation elements use when the page doesn't take over speaking. One per page: there is one voice. */
|
|
71
|
+
export declare function getSpeechController(): SpeechController;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared by every element. The host page can't style anything inside a shadow root, but inherited properties (font,
|
|
3
|
+
* color, line-height, letter-spacing, text-transform …) still flow in from the host element. `.root`, the first element
|
|
4
|
+
* of every template, resets all of them and sets octolearn's values, so the look doesn't depend on the page.
|
|
5
|
+
*
|
|
6
|
+
* Ported from Bootstrap 5.3 (as configured by octolearn: Montserrat, 14px root font size) and octolearn's global.scss:
|
|
7
|
+
* .btn, .btn-octolearn-success, .btn.orange, .form-control and .visually-hidden.
|
|
8
|
+
*/
|
|
9
|
+
export declare const baseStyles: import("lit").CSSResult[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The controls of design D, shared by every element in that look:
|
|
3
|
+
* - `.pill` (white, dark outline) and `.pill.primary` (orange "Fertig" / "Weiter")
|
|
4
|
+
* - `.listen` round orange speaker button (96 px; `.listen.small` 56 px white with outline, `.listen.medium` 80 px)
|
|
5
|
+
* - `.field` the big answer field (84 px, dark border, cream ring) with an `<input>` inside
|
|
6
|
+
* - `.hint` the small grey help line, `kbd` for keys
|
|
7
|
+
* - `.prompt` the handwritten instruction, `.label` the uppercase format label
|
|
8
|
+
* - `.keys-only` for hints about keys ("Enter drücken oder"), hidden on touch screens
|
|
9
|
+
* Sizes are D's at 1280 px. Below 560 px of width (the element's `.root`, or the card around it) they shrink to
|
|
10
|
+
* phone sizes; elements that use these parts set `container-type: inline-size` on their `.root`. While the visible area
|
|
11
|
+
* is short (`:host([short])`, a phone's keyboard is open) they shrink further.
|
|
12
|
+
*/
|
|
13
|
+
export declare const dParts: import("lit").CSSResult;
|