Skip to content

implement choice interaction parsing and serialization for QTIEditor#6012

Open
Abhishek-Punhani wants to merge 4 commits into
learningequality:unstablefrom
Abhishek-Punhani:Issue5965
Open

implement choice interaction parsing and serialization for QTIEditor#6012
Abhishek-Punhani wants to merge 4 commits into
learningequality:unstablefrom
Abhishek-Punhani:Issue5965

Conversation

@Abhishek-Punhani

Copy link
Copy Markdown
Member

Summary

Implemented the QTI declaration serialization framework, base interaction architecture, and the complete Choice Interaction editor.

This PR adds:

  • QTIDeclaration.js — Central class representing a qti-response-declaration element. Handles parsing from XML and generating valid DOM nodes for serialization.
  • ValidationMessage.vue — KDS-compliant UI component for displaying inline validation errors without Vuetify dependencies.
  • useInteraction.js — Base composable that provides standard reactive state, XML parsing/assembly, and validation lifecycle for all future QTI interaction plugins.
  • ChoiceInteractionDescriptor.js — The core logic module for Choice interactions.
  • useChoiceInteraction.js — Composable extending the base interaction to provide all structural and field mutations for choice authoring (e.g., addChoice, toggleCorrectChoice, moveChoiceUp).
  • ChoiceInteractionEditor.vue — UI component for Single Select and Multi Select questions. Features real-time XML synchronization, responsive collapsible toolbars, and localized error handling.
  • assembleItem.js / parseItem.js — Utilities updated to support dynamic XML node building and HTML prompt extraction.

References

Closes #5978

Reviewer guidance

Navigate to the QTI demo page. Test both Single Selection and Multiple Selection modes (pre-populated in the first two cards). Verify that adding, removing, reordering, and editing choices works flawlessly.

AI usage

Used Antigravity for final review and nitpicks.

@learning-equality-bot

Copy link
Copy Markdown

👋 Hi @Abhishek-Punhani, thanks for contributing!

For the review process to begin, please verify that the following is satisfied:

  • Contribution is aligned with our contributing guidelines

  • Pull request description has correctly filled AI usage section & follows our AI guidance:

    AI guidance

    State explicitly whether you didn't use or used AI & how.

    If you used it, ensure that the PR is aligned with Using AI as well as our DEEP framework. DEEP asks you:

    • Disclose — Be open about when you've used AI for support.
    • Engage critically — Question what is generated. Review code for correctness and unnecessary complexity.
    • Edit — Review and refine AI output. Remove unnecessary code and verify it still works after your edits.
    • Process sharing — Explain how you used the AI so others can learn.

    Examples of good disclosures:

    "I used Claude Code to implement the component, prompting it to follow the pattern in ComponentX. I reviewed the generated code, removed unnecessary error handling, and verified the tests pass."

    "I brainstormed the approach with Gemini, then had it write failing tests for the feature. After reviewing the tests, I used Claude Code to generate the implementation. I refactored the output to reduce verbosity and ran the full test suite."

Also check that issue requirements are satisfied & you ran pre-commit locally.

Pull requests that don't follow the guidelines will be closed.

Reviewer assignment can take up to 2 weeks.

@AlexVelezLl AlexVelezLl requested a review from rtibblesbot June 30, 2026 21:50
@learning-equality-bot

Copy link
Copy Markdown

📢✨ Before we assign a reviewer, we'll turn on @rtibblesbot to pre-review. Its comments are generated by an LLM, and should be evaluated accordingly.

@rtibblesbot rtibblesbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synthesized review combining core-logic and frontend-pattern passes. Solid round-trip serialization tests and clean composable design overall; two blocking issues need fixing before merge, plus a few design/duplication concerns worth addressing.

CI passing. Manual QA was required (UI files changed) but did not complete — no visual/a11y verification was performed, so this is not approved on UI grounds.

Blocking:

  • XML attribute injection in reassembleItemXml (parseItem.js) — unescaped &/</" in title/identifier corrupts raw_data on every edit.
  • runValidation() never fires on prompt/choice blur, missing an explicit acceptance criterion for #5978.

Suggestions:

  • ChoiceInteractionEditor.vue duplicates AnswersEditor.vue's CSS/markup/computed block near-verbatim instead of sharing it — including falling back to Options API just to copy buttonAppearanceOverrides, and JS-based hover tracking instead of the established $computedClass :hover pattern.
  • Issue #5978 specified standalone parse.js/validate.js modules; this PR consolidates them into a ChoiceInteractionDescriptor class instead (not blocking — functionally equivalent and consistent with useInteractionDescriptor.js).

Nitpicks: two live ChoiceInteractionDescriptor instances in index.js/useChoiceInteraction.js; unused promptPlaceholder/choiceContentPlaceholder i18n strings; stray data-test attribute not used by any test.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran a phased review pipeline over the pull request diff:

  • Classified the diff to select review passes (core, frontend, backend) and whether manual QA was required
  • Core review pass checked correctness, design, architecture, testing, completeness, and DRY/SRP/Rule-of-Three principles
  • Specialized frontend/backend review passes applied framework-specific lenses where those files changed
  • For UI changes: manual QA and an accessibility audit against a live dev server, when available
  • Checked CI status and linked issue acceptance criteria
  • Synthesized one review from those passes and chose the verdict from the findings, CI status, and QA evidence

* @param {string[]} params.responseDeclarations - Array of serialized declaration XML strings
* @returns {string} Full QTI XML string
*/
export function reassembleItemXml({ identifier, title, language, bodyXml, responseDeclarations }) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: reassembleItemXml interpolates identifier/title/language straight into "..."-delimited XML attributes (e.g. line 115 identifier="${id}") with no escaping. These values come from getAttribute() in parseItem (line 58-60 above), which returns decoded text — an item titled Math & Science "Quiz" produces an unescaped &/" that either breaks well-formedness or terminates the attribute early. This is on the live write path (QTIItemEditor/index.vue emitRawDataupdateItemRawDataitem.raw_data), so any edit to an item whose title/identifier contains &, <, or " silently corrupts raw_data and fails to re-parse on next load. buildXmlNode/XMLSerializer (used elsewhere in assembleItem.js) already handles this correctly — build the assessment-item root the same way, or at minimum escape &/</" before interpolating.

} catch {
return { prompt: '', choices: [] };
}
function closeQuestion() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: Acceptance criteria for #5978 require calling runValidation() on blur of the prompt RTE and each choice RTE. closeQuestion (here) and closeChoice (line 339) are wired to TipTapEditor's @minimize event — the de-facto blur signal (lines 24, 172) — but neither calls runValidation(). The only call sites are onToggleCorrect/onAddChoice/onRemoveChoice (404-424), none of which cover leaving the prompt or a choice empty. An author can leave the prompt or a choice blank, click away, and see no validation error until an unrelated action triggers it. Add runValidation() to both functions.

},
},

computed: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This computed: {} block with buttonAppearanceOverrides() is a byte-for-byte copy of AnswersEditor.vue's Options API computed of the same name (lines 271-282 there), including the legacy this.$themePalette access pattern. Everything else in this file is Composition API in setup() — this didn't need to fall back to Options API; themePalette() from kolibri-design-system/lib/styles/theme can be called inside setup() (see CommunityLibraryStatusButton.vue, SubmitToCommunityLibrarySidePanel/Box.vue) and used in a computed() there instead.

class="answer-border"
role="listitem"
:class="{ 'is-clickable': mode === 'edit' && openChoiceId !== answer.id }"
:style="{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This entire choice-card block (markup, CSS classes .answer-border/.answer-card-text/.answer-layout/.answer-selection/.answer-content/.answer-actions, and the hover handling below) duplicates channelEdit/components/AnswersEditor/AnswersEditor.vue almost verbatim, including identical pixel values in the style rules (lines 586-664). Worth extracting the shared row layout/CSS into a common sub-component or SCSS partial so the two don't drift — this PR's hover-handling regression (next comment) is exactly the kind of drift that duplication invites.

: null,
}"
@click="onRowClick($event, answer.id)"
@mouseenter="hoveredId = answer.id"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Hover state here is tracked via a hoveredId ref + mouseenter/mouseleave listeners per row. AnswersEditor.vue (the component this was copied from) handles the same affordance with $computedClass + a :hover pseudo-selector key (see answerClasses(), AnswersEditor.vue:315-322) — pure CSS, no per-row listeners or reactive re-renders. Consider following that existing pattern instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QTI] Implement choice interaction editor

2 participants