feat(exercise): add open-ended thumbs up/down question type (#780)

* feat(ui): add THUMBS question type renderer

- Add seven new renderer files under renderers/thumbs/:
  thumbs-correct.ts, take.svelte, edit.svelte, preview.svelte,
  thumbs-review-toggle.svelte, review.svelte, submission.svelte
- Register THUMBS in renderer-contract.ts with edit/take/preview/submission/review
- Export ThumbsEdit, ThumbsTake, ThumbsPreview, ThumbsSubmission,
  ThumbsReview from exercise-question/index.ts
- Add THUMBS_FIXTURE, four thumbs label keys, and Thumbs story
  in storybook exercise-question files
- Add THUMBS case to question-type-modes.svelte submission builder

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* feat(question-types): add THUMBS contracts, scoring, and seed

Introduce the THUMBS question type (id 14) with boolean answer data,
codec, auto-grading, Zod validation, DB seed/schema docs, and binary
normalize support shared with TRUE_FALSE. Dashboard wiring is deferred.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* feat(dashboard): wire THUMBS question type into exercise UI

Connect THUMBS option sync in edit/view modes, labels, picker icon,
and locale strings so the existing contracts surface in the dashboard.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): polish thumbs question UI and move premium upgrade icon

Widen thumbs Yes/No toggles, inherit selected icon color via custom class,
replace badge-based review correctness with solid fill states and bare
Check/X icons, add a success pill in preview, and place the premium
upgrade icon at the end of the question type select row.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): align thumbs review colors with prototypes

Tune correct/incorrect selected states so review buttons use dark
emerald/red content colors on solid fills, matching the design comps.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): address remaining PR review comments

Use a shared timestamp local in thumbs edit fallback options.
Move border-beam animation-delay after the animation shorthand.
Alias --spacing to --ui-spacing for tw-animate compatibility.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(db): sync question_type sequence before seeding new types

Avoid duplicate key errors when question_type_id_seq lags behind rows
inserted via restore or explicit ids (e.g. THUMBS on restored databases).

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(exercise): resolve thumbs review label ICU crash

Remove {value} ICU placeholder from thumbs.review.correct_answer_label
since labels are resolved at map-build time without context. Append the
dynamic yes/no label in thumbs-review-toggle instead.

Also bind question points through the questionnaire store index to avoid
the Svelte non-reactive property binding warning in edit-mode.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* docs(agent): add THUMBS guidance and sync question type docs

Add shared agent question-type guide helpers in @cio/question-types and
use them across the teacher prompt, add/update_questions tool schemas,
legacy ai-assistant schemas, and MCP exercise tools.

- Document when to use THUMBS (Yes/No sentiment) vs TRUE_FALSE (factual)
- Fix stale MCP question type ids and add THUMBS=14
- Exclude disabled MATCHING/HOTSPOT from enabled type lists

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(thumbs): clarify auto-grading and add editable option labels

THUMBS is auto-graded like TRUE_FALSE; the difference is thumbs UI with
customizable Yes/No-style labels and the teacher selecting the correct
option. Render take/preview/review from question.options labels and add
label inputs in the thumbs edit renderer. Update agent and MCP docs to
stop implying THUMBS is manual or non-gradable.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* chore(mcp): bump version to 0.0.10

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(question-types): make THUMBS open-ended with no correct answer

THUMBS is a sentiment/poll question type, not an auto-graded binary like
TRUE_FALSE. Remove correct-answer UI, scoring, and validation; update agent
and MCP docs accordingly.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(dashboard): stop treating THUMBS as auto-graded in editor validation

Normalize thumbs questions on load/save, show an Open-ended badge instead
of Auto, and only enforce points/correct-answer rules on auto-gradable types.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* revert(dashboard): remove unsolicited open-ended grading badge

THUMBS uses the existing Manual badge like other non-auto-gradable types.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(exercise): show question title validation errors in editor

Display mapped title errors beneath the question title input instead of
only highlighting the question card border. Pass the exercise entity to
mapZodErrorsToTranslations so messages resolve to validations.exercise.*
keys, and use a translated snackbar key for save validation failures.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(mcp): build workspace deps before MCP publish bundle

The MCP esbuild step imports @cio/question-types and @cio/utils dist
outputs. CI failed on a clean checkout because mcp:build only built the
MCP package itself. Build those dependencies first so 0.0.10 can publish.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
Rotimi Best
2026-07-31 14:59:38 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 0abdfe2102
commit 20e34e29dd
69 changed files with 1022 additions and 166 deletions
@@ -200,6 +200,14 @@
];
}
function makeThumbsOptions() {
const ts = Date.now();
return [
{ id: `${ts}-yes-form`, label: 'Yes', value: 'true', isCorrect: false },
{ id: `${ts}-no-form`, label: 'No', value: 'false', isCorrect: false }
];
}
function onQuestionTypeChange(questionId: string | number, value: string) {
if (!value) return;
@@ -218,6 +226,9 @@
const correctValue = (current.settings as { correctValue?: boolean })?.correctValue ?? true;
nextSettings = { ...nextSettings, correctValue };
nextOptions = makeTrueFalseOptions(correctValue);
} else if (nextQuestionTypeKey === QUESTION_TYPE_KEY.THUMBS) {
nextSettings = { ...nextSettings };
nextOptions = makeThumbsOptions();
} else if (questionTypeSupportsOptions(nextQuestionTypeKey)) {
const hasActiveOptions = nextOptions.some((option) => !option.deletedAt);
if (!hasActiveOptions) {
@@ -276,7 +287,8 @@
let nextSettings = nextQuestion.settings ?? {};
let nextOptions = mappedOptions;
if (getQuestionTypeKey(current) === QUESTION_TYPE_KEY.TRUE_FALSE) {
const currentQuestionTypeKey = getQuestionTypeKey(current);
if (currentQuestionTypeKey === QUESTION_TYPE_KEY.TRUE_FALSE) {
const correctValue = resolveTrueFalseCorrectValue(nextSettings, nextOptions);
nextSettings = { ...nextSettings, correctValue };
nextOptions = syncTrueFalseOptions(nextOptions, correctValue);
@@ -411,15 +423,16 @@
{/snippet}
{#snippet questionEditor(question, index)}
{@const questionStoreIndex = $questionnaire.questions.findIndex((item) => item.id === question.id)}
<QuestionContainer
elementId={getQuestionElementId(question.id)}
key={String(question.id ?? `new-${index}`)}
onClose={onInitDeleteClicked(question.id)}
scrollToQuestion={shouldScrollToLast(question.id, $questionnaire.questions)}
bind:points={question.points}
bind:points={$questionnaire.questions[questionStoreIndex].points}
hasError={!!errors[question.id]}
errorMsg={getQuestionErrorMsg(errors, question, 'points')}
pointsHint={requiresPositivePointsForAutoGrade
pointsHint={isQuestionAutoGradable(question) && requiresPositivePointsForAutoGrade
? $t('course.navItem.lessons.exercises.all_exercises.points_required_auto_grade')
: null}
onPointsChange={() => {
@@ -444,6 +457,7 @@
<div class="mt-2 flex flex-col">
<ExerciseQuestion.QuestionRenderer
showContainer={false}
titleError={getQuestionErrorMsg(errors, question, 'title')}
contract={{
mode: 'edit',
question: toExerciseQuestionModel(question),
@@ -1,6 +1,11 @@
import { getQuestionAnswerKey, getQuestionTypeKey, questionTypeSupportsOptions } from './question-type-utils';
import { QUESTION_TYPE_KEY, resolveTrueFalseCorrectValue, syncTrueFalseOptions } from '@cio/question-types';
import {
QUESTION_TYPE_KEY,
normalizeThumbsQuestion,
resolveTrueFalseCorrectValue,
syncTrueFalseOptions
} from '@cio/question-types';
import type { Question } from '$features/course/types';
import type { QuestionnaireMetaData } from './store';
import type { TExerciseUpdate } from '@cio/utils/validation/exercise';
@@ -174,6 +179,25 @@ export function transformQuestionsToApiFormat(
};
}
if (questionTypeKey === QUESTION_TYPE_KEY.THUMBS) {
const normalized = normalizeThumbsQuestion({
settings,
options: formattedOptions
});
return {
id: normalizeId(q.id),
question: q.title,
points: q.points || 0,
questionTypeId: q.questionTypeId,
exerciseSectionId: q.exerciseSectionId ?? null,
order: typeof (q as { order?: number }).order === 'number' ? (q as { order: number }).order : index + 1,
settings: normalized.settings,
options: normalized.options,
...(shouldIncludeDeleted && q.deletedAt ? { deletedAt: q.deletedAt } : {})
};
}
return {
id: normalizeId(q.id),
question: q.title,
@@ -125,6 +125,17 @@ export function getExerciseQuestionLabels(): ExerciseQuestionLabels {
'true_false.preview.correct_value_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.true_false.preview.correct_value_label'
),
'thumbs.yes_label': t.get('course.navItem.lessons.exercises.all_exercises.shared_question.thumbs.yes_label'),
'thumbs.no_label': t.get('course.navItem.lessons.exercises.all_exercises.shared_question.thumbs.no_label'),
'thumbs.edit.positive_option_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.thumbs.edit.positive_option_label'
),
'thumbs.edit.negative_option_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.thumbs.edit.negative_option_label'
),
'thumbs.preview.helper': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.thumbs.preview.helper'
),
'short_answer.edit.instructions_placeholder': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.short_answer.edit.instructions_placeholder'
),
@@ -39,7 +39,7 @@
{#snippet upgradeButton({ typeEntry })}
<button
type="button"
class="ui:flex ui:w-full ui:cursor-pointer ui:select-none ui:items-center ui:gap-2 ui:rounded-sm ui:py-1.5 ui:pl-2 ui:pr-8 ui:text-sm ui:outline-hidden ui:hover:bg-accent ui:hover:text-accent-foreground ui:relative"
class="ui:relative ui:flex ui:w-full ui:cursor-pointer ui:select-none ui:items-center ui:justify-between ui:gap-2 ui:rounded-sm ui:px-2 ui:py-1.5 ui:text-sm ui:outline-hidden ui:hover:bg-accent ui:hover:text-accent-foreground"
onclick={(clickEvent) => {
clickEvent.preventDefault();
clickEvent.stopPropagation();
@@ -47,8 +47,8 @@
}}
title={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.premium_question_type')}
>
<PremiumIcon size={16} class="ui:text-blue-700 ui:dark:text-white ui:shrink-0" />
<span>{getExerciseEditorQuestionTypeLabel(typeEntry)}</span>
<PremiumIcon size={16} class="ui:ml-auto ui:shrink-0 ui:text-blue-700 ui:dark:text-white" />
</button>
{/snippet}
@@ -24,7 +24,8 @@ export function getExerciseEditorQuestionTypeLabel(
const OPTION_QUESTION_TYPE_KEYS = new Set<ExerciseQuestionTypeKey>([
QUESTION_TYPE_KEY.RADIO,
QUESTION_TYPE_KEY.CHECKBOX,
QUESTION_TYPE_KEY.TRUE_FALSE
QUESTION_TYPE_KEY.TRUE_FALSE,
QUESTION_TYPE_KEY.THUMBS
]);
export function getQuestionTypeId(question: Partial<Question>): number {
@@ -405,6 +405,7 @@
switch (answerValue.type) {
case 'RADIO':
case 'TRUE_FALSE':
case 'THUMBS':
case 'NUMERIC':
case 'STAR':
return true;
@@ -62,7 +62,7 @@
import * as Alert from '@cio/ui/base/alert';
import * as RadioGroup from '@cio/ui/base/radio-group';
import InfoIcon from '@lucide/svelte/icons/info';
import { isAutoGradableQuestionType } from '@cio/question-types';
import { isAutoGradableQuestionType, isAutoGradableQuestionTypeId } from '@cio/question-types';
import * as Dialog from '@cio/ui/base/dialog';
import type { ExerciseSectionState } from '$features/course/components/exercise/store';
import {
@@ -368,18 +368,23 @@
});
if (!zodResult.success || Object.keys(sectionValidationErrors).length > 0) {
const zodErrors = !zodResult.success ? mapZodErrorsToTranslations(zodResult.error) : {};
const zodErrors = !zodResult.success ? mapZodErrorsToTranslations(zodResult.error, 'exercise') : {};
const questionErrors = mapZodErrorsToQuestionErrors(zodErrors, $questionnaire.questions);
questionnaireValidation.set({
...questionErrors,
...sectionValidationErrors
});
snackbar.error('Please fix all validation errors before saving');
snackbar.error('snackbar.exercise.validation_errors');
return;
}
if (requiresPositivePointsForAutoGrade) {
const zeroPointQuestions = ($questionnaire.questions ?? []).filter((q) => !q.deletedAt && Number(q.points) === 0);
const zeroPointQuestions = ($questionnaire.questions ?? []).filter(
(question) =>
!question.deletedAt &&
Number(question.points) === 0 &&
isAutoGradableQuestionTypeId(getQuestionTypeId(question))
);
if (zeroPointQuestions.length > 0) {
const pointErrors: Record<string, { option?: string; title?: string; points?: string }> = {};
@@ -430,7 +435,7 @@
// Check if there are validation errors from the API
if (Object.keys(exerciseApi.errors || {}).length > 0) {
console.log('Validation errors from API:', exerciseApi.errors);
snackbar.error('Please fix all validation errors before saving');
snackbar.error('snackbar.exercise.validation_errors');
isSaving = false;
return;
}
@@ -1,10 +1,15 @@
import type { ExerciseSectionAfterBehavior, ExerciseSectionColorTheme } from '@cio/question-types';
import { QUESTION_TYPE_KEY, normalizeThumbsQuestion } from '@cio/question-types';
import {
clearQuestionnaireValidation,
questionnaire,
questionnaireMetaData
} from '$features/course/components/exercise/store';
import { getQuestionTypeId, getQuestionTypeOptionById } from '$features/course/components/exercise/question-type-utils';
import {
getQuestionTypeId,
getQuestionTypeKey,
getQuestionTypeOptionById
} from '$features/course/components/exercise/question-type-utils';
import type { Exercise } from '$features/course/utils/types';
import type { ExerciseSectionState } from '$features/course/components/exercise/store';
@@ -34,13 +39,27 @@ export function hydrateExercisePageData(exercise: Exercise, exerciseId: string)
if (exerciseQuestions.length > 0) {
const mappedQuestions: Question[] = exerciseQuestions.map((question) => {
const questionType = getQuestionTypeOptionById(getQuestionTypeId(question));
return {
const baseQuestion = {
...question,
exerciseSectionId: question.exerciseSectionId ?? null,
questionTypeId: questionType.id,
questionType
};
if (getQuestionTypeKey(baseQuestion) !== QUESTION_TYPE_KEY.THUMBS) {
return baseQuestion;
}
const normalized = normalizeThumbsQuestion({
settings: (baseQuestion as Question & { settings?: Record<string, unknown> }).settings,
options: baseQuestion.options
});
return {
...baseQuestion,
settings: normalized.settings,
options: normalized.options
};
});
if (sections.length > 0) {
@@ -7,6 +7,7 @@ const QUESTION_TYPE_LABEL_KEYS: Partial<Record<QuestionTypeKey, string>> = {
[QUESTION_TYPE_KEY.CHECKBOX]: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_types.multiple',
[QUESTION_TYPE_KEY.TEXTAREA]: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_types.paragraph',
[QUESTION_TYPE_KEY.TRUE_FALSE]: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_types.true_false',
[QUESTION_TYPE_KEY.THUMBS]: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_types.thumbs',
[QUESTION_TYPE_KEY.SHORT_ANSWER]:
'course.navItem.lessons.exercises.all_exercises.edit_mode.question_types.short_answer',
[QUESTION_TYPE_KEY.NUMERIC]: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_types.numeric',
@@ -321,6 +321,7 @@
"creating_new": "Oprettelse af nye oversættelser mislykkedes"
},
"exercise": {
"validation_errors": "Ret alle valideringsfejl, før du gemmer.",
"points_required_auto_grade": "Angiv en positiv pointværdi for hvert spørgsmål, før du gemmer.",
"success": "Gemt",
"submission_updated": "Indsendelse opdateret til",
@@ -1517,6 +1518,7 @@
"multiple": "Flere svar",
"paragraph": "Afsnit",
"true_false": "Sandt/falsk",
"thumbs": "Tommel op/ned",
"short_answer": "Kort svar",
"numeric": "Numerisk svar",
"fill_blank": "Udfyld det tomme felt",
@@ -1705,6 +1707,17 @@
"correct_answer_label": "Korrekt svar"
}
},
"thumbs": {
"yes_label": "Ja",
"no_label": "Nej",
"edit": {
"positive_option_label": "Tommel op-etiket",
"negative_option_label": "Tommel ned-etiket"
},
"preview": {
"helper": "Åben — elever vælger tommel op eller ned."
}
},
"checkbox": {
"edit": {
"helper": "Marker en eller flere muligheder som korrekte.",
@@ -321,6 +321,7 @@
"creating_new": "Das Erstellen neuer Übersetzungen ist fehlgeschlagen"
},
"exercise": {
"validation_errors": "Bitte beheben Sie alle Validierungsfehler, bevor Sie speichern.",
"points_required_auto_grade": "Legen Sie für jede Frage einen positiven Punktwert fest, bevor Sie speichern.",
"success": "Erfolgreich gespeichert",
"submission_updated": "Einreichung aktualisiert auf",
@@ -1517,6 +1518,7 @@
"multiple": "Mehrere Antworten",
"paragraph": "Absatz",
"true_false": "Richtig/Falsch",
"thumbs": "Daumen hoch/runter",
"short_answer": "Kurze Antwort",
"numeric": "Numerische Antwort",
"fill_blank": "Füllen Sie die Lücke aus",
@@ -1705,6 +1707,17 @@
"correct_answer_label": "Richtige Antwort"
}
},
"thumbs": {
"yes_label": "Ja",
"no_label": "Nein",
"edit": {
"positive_option_label": "Daumen-hoch-Label",
"negative_option_label": "Daumen-runter-Label"
},
"preview": {
"helper": "Offen — Lernende wählen Daumen hoch oder runter."
}
},
"checkbox": {
"edit": {
"helper": "Markieren Sie eine oder mehrere Optionen als richtig.",
@@ -2627,6 +2627,7 @@
"multiple": "Multiple answers",
"paragraph": "Paragraph",
"true_false": "True/False",
"thumbs": "Thumbs up/down",
"short_answer": "Short answer",
"numeric": "Numeric answer",
"fill_blank": "Fill in the blank",
@@ -2772,6 +2773,17 @@
"correct_value_label": "Correct value"
}
},
"thumbs": {
"yes_label": "Yes",
"no_label": "No",
"edit": {
"positive_option_label": "Thumbs up label",
"negative_option_label": "Thumbs down label"
},
"preview": {
"helper": "Open-ended — students choose thumbs up or down."
}
},
"short_answer": {
"edit": {
"instructions_placeholder": "Optional instructions"
@@ -4181,6 +4193,7 @@
}
},
"exercise": {
"validation_errors": "Please fix all validation errors before saving.",
"points_required_auto_grade": "Set a positive point value for every question before saving.",
"passed_requires_points": "Passed completion needs at least one point available.",
"success": "Saved Successfully",
@@ -321,6 +321,7 @@
"creating_new": "Error al crear nuevas traducciones"
},
"exercise": {
"validation_errors": "Corrija todos los errores de validación antes de guardar.",
"points_required_auto_grade": "Asigne un valor de puntos positivo a cada pregunta antes de guardar.",
"success": "Guardado exitosamente",
"submission_updated": "Envío actualizado a",
@@ -1517,6 +1518,7 @@
"multiple": "Múltiples respuestas",
"paragraph": "Párrafo",
"true_false": "Verdadero/Falso",
"thumbs": "Pulgares arriba/abajo",
"short_answer": "respuesta corta",
"numeric": "respuesta numérica",
"fill_blank": "Completa el espacio en blanco",
@@ -1705,6 +1707,17 @@
"correct_answer_label": "respuesta correcta"
}
},
"thumbs": {
"yes_label": "Sí",
"no_label": "No",
"edit": {
"positive_option_label": "Etiqueta de pulgar arriba",
"negative_option_label": "Etiqueta de pulgar abajo"
},
"preview": {
"helper": "Abierta — los estudiantes eligen pulgar arriba o abajo."
}
},
"checkbox": {
"edit": {
"helper": "Marque una o más opciones como correctas.",
@@ -321,6 +321,7 @@
"creating_new": "La création de nouvelles traductions a échoué"
},
"exercise": {
"validation_errors": "Veuillez corriger toutes les erreurs de validation avant d'enregistrer.",
"points_required_auto_grade": "Définissez un nombre de points positif pour chaque question avant d'enregistrer.",
"success": "Enregistré avec succès",
"submission_updated": "Soumission mise à jour pour",
@@ -1517,6 +1518,7 @@
"multiple": "Plusieurs réponses",
"paragraph": "Paragraphe",
"true_false": "Vrai/Faux",
"thumbs": "Pouce vers le haut/bas",
"short_answer": "Réponse courte",
"numeric": "Réponse numérique",
"fill_blank": "Remplissez le vide",
@@ -1705,6 +1707,17 @@
"correct_answer_label": "Bonne réponse"
}
},
"thumbs": {
"yes_label": "Oui",
"no_label": "Non",
"edit": {
"positive_option_label": "Libellé pouce vers le haut",
"negative_option_label": "Libellé pouce vers le bas"
},
"preview": {
"helper": "Ouverte — les apprenants choisissent pouce vers le haut ou vers le bas."
}
},
"checkbox": {
"edit": {
"helper": "Marquez une ou plusieurs options comme correctes.",
@@ -321,6 +321,7 @@
"creating_new": "नए अनुवाद बनाना विफल रहा"
},
"exercise": {
"validation_errors": "सहेजने से पहले सभी सत्यापन त्रुटियों को ठीक करें।",
"points_required_auto_grade": "सहेजने से पहले प्रत्येक प्रश्न के लिए धनात्मक अंक निर्धारित करें।",
"success": "सफलतापूर्वक सहेजा गया",
"submission_updated": "सबमिशन को अद्यतन किया गया",
@@ -1517,6 +1518,7 @@
"multiple": "एकाधिक उत्तर",
"paragraph": "अनुच्छेद",
"true_false": "सत्य/असत्य",
"thumbs": "अंगूठा ऊपर/नीचे",
"short_answer": "संक्षिप्त उत्तर",
"numeric": "संख्यात्मक उत्तर",
"fill_blank": "रिक्त स्थान भरें",
@@ -1705,6 +1707,17 @@
"correct_answer_label": "सही उत्तर"
}
},
"thumbs": {
"yes_label": "हाँ",
"no_label": "नहीं",
"edit": {
"positive_option_label": "अंगूठा ऊपर लेबल",
"negative_option_label": "अंगूठा नीचे लेबल"
},
"preview": {
"helper": "खुला प्रश्न — छात्र अंगूठा ऊपर या नीचे चुनते हैं।"
}
},
"checkbox": {
"edit": {
"helper": "एक या अधिक विकल्पों को सही के रूप में चिह्नित करें.",
@@ -321,6 +321,7 @@
"creating_new": "Tworzenie nowych tłumaczeń nie powiodło się"
},
"exercise": {
"validation_errors": "Popraw wszystkie błędy walidacji przed zapisaniem.",
"points_required_auto_grade": "Ustaw dodatnią liczbę punktów dla każdego pytania przed zapisaniem.",
"success": "Zapisano pomyślnie",
"submission_updated": "Zgłoszenie zaktualizowane do",
@@ -1517,6 +1518,7 @@
"multiple": "Wiele odpowiedzi",
"paragraph": "Akapit",
"true_false": "Prawda/Fałsz",
"thumbs": "Kciuk w górę/w dół",
"short_answer": "Krótka odpowiedź",
"numeric": "Odpowiedź numeryczna",
"fill_blank": "Wypełnij puste miejsce",
@@ -1705,6 +1707,17 @@
"correct_answer_label": "Prawidłowa odpowiedź"
}
},
"thumbs": {
"yes_label": "Tak",
"no_label": "Nie",
"edit": {
"positive_option_label": "Etykieta kciuka w górę",
"negative_option_label": "Etykieta kciuka w dół"
},
"preview": {
"helper": "Otwarte — uczniowie wybierają kciuk w górę lub w dół."
}
},
"checkbox": {
"edit": {
"helper": "Zaznacz jedną lub więcej opcji jako poprawną.",
@@ -321,6 +321,7 @@
"creating_new": "Falha ao criar novas traduções"
},
"exercise": {
"validation_errors": "Corrija todos os erros de validação antes de salvar.",
"points_required_auto_grade": "Defina um valor de pontos positivo para cada pergunta antes de salvar.",
"success": "Salvo com sucesso",
"submission_updated": "Envio atualizado para",
@@ -1517,6 +1518,7 @@
"multiple": "Múltiplas respostas",
"paragraph": "Parágrafo",
"true_false": "Verdadeiro/Falso",
"thumbs": "Polegar para cima/baixo",
"short_answer": "Resposta curta",
"numeric": "Resposta numérica",
"fill_blank": "Preencha o espaço em branco",
@@ -1705,6 +1707,17 @@
"correct_answer_label": "Resposta correta"
}
},
"thumbs": {
"yes_label": "Sim",
"no_label": "Não",
"edit": {
"positive_option_label": "Rótulo de polegar para cima",
"negative_option_label": "Rótulo de polegar para baixo"
},
"preview": {
"helper": "Aberta — os alunos escolhem polegar para cima ou para baixo."
}
},
"checkbox": {
"edit": {
"helper": "Marque uma ou mais opções como corretas.",
@@ -321,6 +321,7 @@
"creating_new": "Не удалось создать новые переводы."
},
"exercise": {
"validation_errors": "Исправьте все ошибки проверки перед сохранением.",
"points_required_auto_grade": "Установите положительное количество баллов для каждого вопроса перед сохранением.",
"success": "Сохранено успешно",
"submission_updated": "Представление обновлено до",
@@ -1517,6 +1518,7 @@
"multiple": "Несколько ответов",
"paragraph": "Параграф",
"true_false": "Верно/Неверно",
"thumbs": "Большой палец вверх/вниз",
"short_answer": "Короткий ответ",
"numeric": "Числовой ответ",
"fill_blank": "Заполните пробел",
@@ -1705,6 +1707,17 @@
"correct_answer_label": "Правильный ответ"
}
},
"thumbs": {
"yes_label": "Да",
"no_label": "Нет",
"edit": {
"positive_option_label": "Подпись «палец вверх»",
"negative_option_label": "Подпись «палец вниз»"
},
"preview": {
"helper": "Открытый вопрос — студенты выбирают палец вверх или вниз."
}
},
"checkbox": {
"edit": {
"helper": "Отметьте один или несколько вариантов как правильные.",
@@ -321,6 +321,7 @@
"creating_new": "Tạo bản dịch mới không thành công"
},
"exercise": {
"validation_errors": "Vui lòng sửa tất cả lỗi xác thực trước khi lưu.",
"points_required_auto_grade": "Đặt điểm dương cho mỗi câu hỏi trước khi lưu.",
"success": "Đã lưu thành công",
"submission_updated": "Đã cập nhật bài nộp vào",
@@ -1517,6 +1518,7 @@
"multiple": "Nhiều câu trả lời",
"paragraph": "Đoạn",
"true_false": "Đúng/Sai",
"thumbs": "Thích/Không thích",
"short_answer": "Câu trả lời ngắn",
"numeric": "Câu trả lời bằng số",
"fill_blank": "Điền vào chỗ trống",
@@ -1705,6 +1707,17 @@
"correct_answer_label": "Câu trả lời đúng"
}
},
"thumbs": {
"yes_label": "Có",
"no_label": "Không",
"edit": {
"positive_option_label": "Nhãn thích",
"negative_option_label": "Nhãn không thích"
},
"preview": {
"helper": "Mở — học viên chọn thích hoặc không thích."
}
},
"checkbox": {
"edit": {
"helper": "Đánh dấu một hoặc nhiều lựa chọn là đúng.",
+9 -5
View File
@@ -444,15 +444,19 @@ Supported `questionTypeId` values for exercise payloads:
- `1` `RADIO` - Single answer
- `2` `CHECKBOX` - Multiple answers
- `3` `TEXTAREA` - Paragraph
- `4` `TRUE_FALSE` - True/False
- `4` `TRUE_FALSE` - True/False (factual statements; options: True, False; auto-graded)
- `5` `SHORT_ANSWER` - Short answer
- `6` `NUMERIC` - Numeric answer
- `7` `FILL_BLANK` - Fill in the blank
- `8` `FILE_UPLOAD` - File upload
- `9` `MATCHING` - Matching
- `10` `ORDERING` - Ordering
- `11` `HOTSPOT` - Hotspot
- `12` `LINK` - Links
- `9` `ORDERING` - Ordering
- `10` `LINK` - Links
- `11` `WORD_BANK` - Word bank
- `12` `STAR` - Star rating
- `13` `VIDEO_RECORDING` - Video recording
- `14` `THUMBS` - Thumbs up/down (open-ended poll with customizable Yes/No labels; no correct answer)
`TRUE_FALSE` is auto-graded with `settings.correctValue` (boolean) and fixed True/False labels for factual statements. `THUMBS` is open-ended (not auto-graded): two customizable Yes/No-style options with thumbs icons for sentiment or poll-style questions — do not set `settings.correctValue`. `MATCHING` and `HOTSPOT` are disabled and must not be used.
## Operational Notes