You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Overview
Add the inline choice interaction's headless layer: its descriptor, parsing, XML assembly and validation. This task has no UI. It fixes the XML shape and the in-memory state that the passage node (#6181) and the editor UI (#6182) build on, and it is fully testable without them.
Complexity: Medium Target branch: unstable
Context
The interaction maps to <qti-inline-choice-interaction>. In the design, an inline choice question has two authored regions:
an optional Question (a rich-text prompt, like the other interactions' prompts)
a Passage of rich text holding one or more dropdowns ("chips"), each with its own plain-text options and one correct answer
Each dropdown is its own interaction with its own response declaration. Carrying several declarations through the editor core, and scoring across them, is #6179.
Text entry is the only other Placement.INLINE interaction and the nearest reference for recovering authored content from the item body (interactions/textEntry/parse.js). Choice is the nearest reference for choices with a correct answer (interactions/choice/).
Kolibri's QTI viewer already renders the interaction (kolibri/plugins/qti_viewer/frontend/components/interactions/InlineChoiceInteraction.vue), and the server's QTI 3.0 XSD validation accepts it.
QTI XML reference
<qti-assessment-itemxmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"identifier="item_k2lm9qaz"title="Question"adaptive="false"time-dependent="false">
<qti-response-declarationidentifier="response_xq7tbn2c"cardinality="single"base-type="identifier">
<qti-correct-response>
<qti-value>choice_a1b2c3d4</qti-value>
</qti-correct-response>
</qti-response-declaration>
<qti-response-declarationidentifier="response_pw4rzk8d"cardinality="single"base-type="identifier">
<qti-correct-response>
<qti-value>choice_m0o0n0aa</qti-value>
</qti-correct-response>
</qti-response-declaration>
<!-- Outcome declarations and response processing are generated by the core (#6179) -->
<qti-item-body>
<!-- Question (optional) -->
<p>Select the correct words to complete the sentence:</p>
<!-- Passage -->
<p>
The Earth
<qti-inline-choice-interactionresponse-identifier="response_xq7tbn2c"shuffle="true">
<qti-inline-choiceidentifier="choice_a1b2c3d4">revolves</qti-inline-choice>
<qti-inline-choiceidentifier="choice_e5f6g7h8">stays</qti-inline-choice>
</qti-inline-choice-interaction>
around the Sun, and the
<qti-inline-choice-interactionresponse-identifier="response_pw4rzk8d"shuffle="true">
<qti-inline-choiceidentifier="choice_s0u0n0aa">Sun</qti-inline-choice>
<qti-inline-choiceidentifier="choice_s0t0a0rs">Stars</qti-inline-choice>
<qti-inline-choiceidentifier="choice_m0o0n0aa">Moon</qti-inline-choice>
</qti-inline-choice-interaction>
orbits the Earth.
</p>
</qti-item-body>
</qti-assessment-item>
Rules:
Every dropdown is one <qti-inline-choice-interaction> with its own response-identifier, bound to its own <qti-response-declaration> with cardinality="single", base-type="identifier" and exactly one <qti-value> in <qti-correct-response>. A dropdown with no correct option yet has no <qti-correct-response>.
Response identifiers are unique within the item and stable across edits: adding, removing or reordering a dropdown must not rename the others. Kolibri stores the learner's answer state keyed by response identifier (AssessmentItem.vue).
Choice identifiers are unique within their dropdown and stable across edits; choices without one get generateRandomSlug('choice').
<qti-inline-choice> content is plain text. The passage around the dropdowns is rich text, and a dropdown can sit inside formatted content (e.g. within bold text or a list item).
shuffle is always "true".
Response declarations appear in the same order as their dropdowns in the passage.
The question and the passage must be distinguishable when the item is parsed back. An item saved with an empty question, or a question that itself contains several paragraphs, must reopen with the same question and passage it was saved with.
The Change
New QtiInteraction.INLINE_CHOICE and QuestionType.INLINE_CHOICE.
An inline choice descriptor under interactions/inlineChoice/ (Descriptor.js, parse.js, validation.js), with Placement.INLINE, registered in interactions/descriptors.js.
Validation reuses the existing ValidationError codes. Each error is attributed to the dropdown it belongs to (by response identifier), and to the option where there is one:
a dropdown with fewer than two options → TOO_FEW_CHOICES
a dropdown with no correct option → NO_CORRECT_ANSWER
an empty option → EMPTY_CHOICE_CONTENT
two options with the same text in one dropdown → DUPLICATE_CHOICE_CONTENT
A passage with no dropdowns is already reported as NO_INTERACTION by validateItemShape. The question is optional, so an empty question is not an error.
Mixing inline choice with another interaction type in the same item.
Acceptance Criteria
General
Parsing and rebuilding an item with a question and several dropdowns returns the same question, passage text, dropdown order, options, correct answers and identifiers
An item with an empty question, and an item whose question has several paragraphs, both reopen with the question and passage they were saved with
Rebuilt XML follows every rule above and passes server-side QTI validation
A dropdown with no correct option is written without <qti-correct-response>, and parses back with no correct option
Removing or reordering a dropdown in the state leaves the other dropdowns' response identifiers unchanged in the rebuilt XML
Each validation case listed above is reported with the response identifier of its dropdown
resolveDescriptor resolves an item containing qti-inline-choice-interaction to the inline choice descriptor and QuestionType.INLINE_CHOICE
The question type selector does not offer Inline choice yet
Testing
parse.spec.js covers the round trips above, including missing identifiers and a dropdown with no correct response
validation.spec.js covers each validation case
Descriptor.spec.js covers matching and question type resolution
Testing
pnpm jest --config jest_config/jest.conf.js contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions
Nearest references: shared/views/QTIEditor/interactions/textEntry/ (inline placement), shared/views/QTIEditor/interactions/choice/ (choices with a correct answer)
AI usage
I used Claude Code to draft this issue from the Figma design and the existing interaction plugins, which it read to ground the Context and rules. I decided how to split the work and reviewed and edited the text.
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Overview
Add the inline choice interaction's headless layer: its descriptor, parsing, XML assembly and validation. This task has no UI. It fixes the XML shape and the in-memory state that the passage node (#6181) and the editor UI (#6182) build on, and it is fully testable without them.
Complexity: Medium
Target branch: unstable
Context
The interaction maps to
<qti-inline-choice-interaction>. In the design, an inline choice question has two authored regions:Each dropdown is its own interaction with its own response declaration. Carrying several declarations through the editor core, and scoring across them, is #6179.
Text entry is the only other
Placement.INLINEinteraction and the nearest reference for recovering authored content from the item body (interactions/textEntry/parse.js). Choice is the nearest reference for choices with a correct answer (interactions/choice/).Kolibri's QTI viewer already renders the interaction (
kolibri/plugins/qti_viewer/frontend/components/interactions/InlineChoiceInteraction.vue), and the server's QTI 3.0 XSD validation accepts it.QTI XML reference
Rules:
<qti-inline-choice-interaction>with its ownresponse-identifier, bound to its own<qti-response-declaration>withcardinality="single",base-type="identifier"and exactly one<qti-value>in<qti-correct-response>. A dropdown with no correct option yet has no<qti-correct-response>.AssessmentItem.vue).generateRandomSlug('choice').<qti-inline-choice>content is plain text. The passage around the dropdowns is rich text, and a dropdown can sit inside formatted content (e.g. within bold text or a list item).shuffleis always"true".The Change
QtiInteraction.INLINE_CHOICEandQuestionType.INLINE_CHOICE.interactions/inlineChoice/(Descriptor.js,parse.js,validation.js), withPlacement.INLINE, registered ininteractions/descriptors.js.parseandbuildXMLround-trip the question, the passage, and every dropdown's options and correct answer, following the rules above. The state should keep dropdowns in passage order, addressable by response identifier, so the passage node ([QTI] Inline choice chip and Insert action in the passage editor #6181) and the options list ([QTI] Inline choice interaction editor #6182) can refer to the same dropdown.ValidationErrorcodes. Each error is attributed to the dropdown it belongs to (by response identifier), and to the option where there is one:TOO_FEW_CHOICESNO_CORRECT_ANSWEREMPTY_CHOICE_CONTENTDUPLICATE_CHOICE_CONTENTNO_INTERACTIONbyvalidateItemShape. The question is optional, so an empty question is not an error.getTypeOptionscontributes nothing). [QTI] Inline choice interaction editor #6182 turns it on.Out of Scope
Acceptance Criteria
General
<qti-correct-response>, and parses back with no correct optionresolveDescriptorresolves an item containingqti-inline-choice-interactionto the inline choice descriptor andQuestionType.INLINE_CHOICETesting
parse.spec.jscovers the round trips above, including missing identifiers and a dropdown with no correct responsevalidation.spec.jscovers each validation caseDescriptor.spec.jscovers matching and question type resolutionTesting
References
shared/views/QTIEditor/interactions/textEntry/(inline placement),shared/views/QTIEditor/interactions/choice/(choices with a correct answer)AI usage
I used Claude Code to draft this issue from the Figma design and the existing interaction plugins, which it read to ground the Context and rules. I decided how to split the work and reviewed and edited the text.