Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest';
import { item as inlineFeedbackItem } from '../../../cutie-example/src/example-items/inline-feedback';
import { item as modalFeedbackItem } from '../../../cutie-example/src/example-items/modal-feedback';
import { item as choiceFeedbackItem } from '../../../cutie-example/src/example-items/standard-choice';
import { item as textEntryItem } from '../../../cutie-example/src/example-items/standard-text-entry';
import { item as textEntryMultiItem } from '../../../cutie-example/src/example-items/text-entry-multi';
import { classifyResponseProcessing } from './responseProcessingClassifier';

/**
Expand Down Expand Up @@ -285,6 +287,44 @@ describe('responseProcessingClassifier', () => {
expect(result.mode).toBe('sumScores');
});

it('should recognize sumScores + mapped response feedback (qti-gt > qti-map-response)', () => {
const doc = createQtiDoc(`
<qti-response-processing>
<qti-set-outcome-value identifier="SCORE">
<qti-sum>
<qti-map-response identifier="RESPONSE"/>
</qti-sum>
</qti-set-outcome-value>

<qti-response-condition>
<qti-response-if>
<qti-gt>
<qti-map-response identifier="RESPONSE"/>
<qti-base-value base-type="float">0</qti-base-value>
</qti-gt>
<qti-set-outcome-value identifier="FEEDBACK">
<qti-multiple>
<qti-variable identifier="FEEDBACK"/>
<qti-base-value base-type="identifier">RESPONSE_correct</qti-base-value>
</qti-multiple>
</qti-set-outcome-value>
</qti-response-if>
<qti-response-else>
<qti-set-outcome-value identifier="FEEDBACK">
<qti-multiple>
<qti-variable identifier="FEEDBACK"/>
<qti-base-value base-type="identifier">RESPONSE_incorrect</qti-base-value>
</qti-multiple>
</qti-set-outcome-value>
</qti-response-else>
</qti-response-condition>
</qti-response-processing>
`);

const result = classifyResponseProcessing(doc);
expect(result.mode).toBe('sumScores');
});

it('should reject sumScores if additional condition sets non-FEEDBACK outcome', () => {
const doc = createQtiDoc(`
<qti-response-processing>
Expand Down Expand Up @@ -338,5 +378,17 @@ describe('responseProcessingClassifier', () => {
const result = classifyResponseProcessing(doc);
expect(result.mode).toBe('allCorrect');
});

it('should classify standard-text-entry.ts as sumScores', () => {
const doc = parseItem(textEntryItem);
const result = classifyResponseProcessing(doc);
expect(result.mode).toBe('sumScores');
});

it('should classify text-entry-multi.ts as sumScores', () => {
const doc = parseItem(textEntryMultiItem);
const result = classifyResponseProcessing(doc);
expect(result.mode).toBe('sumScores');
});
});
});
72 changes: 54 additions & 18 deletions packages/cutie-editor/src/utils/responseProcessingGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ function generateSumScoresXml(
responseIdentifiers,
responseDeclarations,
feedbackIdentifiersUsed,
doc
doc,
new Set(withMapping)
);
for (const fc of feedbackConditions) {
responseProcessing.appendChild(fc);
Expand Down Expand Up @@ -381,7 +382,8 @@ function generateFeedbackProcessingXml(
responseIdentifiers: string[],
responseDeclarations: Map<string, XmlNode>,
feedbackIdentifiersUsed: Set<string>,
doc: Document
doc: Document,
mappedResponseIds?: Set<string>
): Element[] {
const conditions: Element[] = [];

Expand Down Expand Up @@ -413,12 +415,15 @@ function generateFeedbackProcessingXml(
const hasIncorrect = feedbackIds.has(incorrectId);

if (hasCorrect || hasIncorrect) {
// Generate correct/incorrect condition
// For responses with mappings, use qti-map-response > 0 instead of qti-match
// so that feedback agrees with case-insensitive mapped scoring
const useMapResponse = mappedResponseIds?.has(responseId) ?? false;
const condition = createFeedbackCorrectIncorrectCondition(
responseId,
hasCorrect ? correctId : null,
hasIncorrect ? incorrectId : null,
doc
doc,
useMapResponse
);
conditions.push(condition);
}
Expand All @@ -445,40 +450,51 @@ function generateFeedbackProcessingXml(
/**
* Create a feedback condition for correct/incorrect responses.
*
* For unmapped responses (useMapResponse=false), uses qti-match:
* <qti-response-condition>
* <qti-response-if>
* <qti-match>
* <qti-variable identifier="RESPONSE"/>
* <qti-correct identifier="RESPONSE"/>
* </qti-match>
* <qti-set-outcome-value identifier="FEEDBACK">
* <qti-multiple>
* <qti-variable identifier="FEEDBACK"/>
* <qti-base-value base-type="identifier">RESPONSE_correct</qti-base-value>
* </qti-multiple>
* </qti-set-outcome-value>
* <qti-set-outcome-value identifier="FEEDBACK">...</qti-set-outcome-value>
* </qti-response-if>
* <qti-response-else>
* <qti-set-outcome-value identifier="FEEDBACK">
* <qti-multiple>
* <qti-variable identifier="FEEDBACK"/>
* <qti-base-value base-type="identifier">RESPONSE_incorrect</qti-base-value>
* </qti-multiple>
* </qti-set-outcome-value>
* <qti-set-outcome-value identifier="FEEDBACK">...</qti-set-outcome-value>
* </qti-response-else>
* </qti-response-condition>
*
* For mapped responses (useMapResponse=true), uses qti-gt with qti-map-response
* so that feedback agrees with case-insensitive mapped scoring:
* <qti-response-condition>
* <qti-response-if>
* <qti-gt>
* <qti-map-response identifier="RESPONSE"/>
* <qti-base-value base-type="float">0</qti-base-value>
* </qti-gt>
* <qti-set-outcome-value identifier="FEEDBACK">...</qti-set-outcome-value>
* </qti-response-if>
* <qti-response-else>
* <qti-set-outcome-value identifier="FEEDBACK">...</qti-set-outcome-value>
* </qti-response-else>
* </qti-response-condition>
*/
function createFeedbackCorrectIncorrectCondition(
responseId: string,
correctFeedbackId: string | null,
incorrectFeedbackId: string | null,
doc: Document
doc: Document,
useMapResponse: boolean = false
): Element {
const condition = doc.createElementNS(QTI_NAMESPACE, 'qti-response-condition');

// Response if - when correct
const responseIf = doc.createElementNS(QTI_NAMESPACE, 'qti-response-if');
responseIf.appendChild(createMatchElement(responseId, doc));
if (useMapResponse) {
responseIf.appendChild(createMapResponseGtZeroElement(responseId, doc));
} else {
responseIf.appendChild(createMatchElement(responseId, doc));
}

if (correctFeedbackId) {
responseIf.appendChild(createSetFeedbackElement(correctFeedbackId, doc));
Expand All @@ -496,6 +512,26 @@ function createFeedbackCorrectIncorrectCondition(
return condition;
}

/**
* Create a qti-gt element that checks if a mapped response score is greater than 0.
* Used for feedback conditions on responses with mappings, so that feedback
* uses the same case-insensitive matching as the scoring.
*/
function createMapResponseGtZeroElement(identifier: string, doc: Document): Element {
const gt = doc.createElementNS(QTI_NAMESPACE, 'qti-gt');

const mapResponse = doc.createElementNS(QTI_NAMESPACE, 'qti-map-response');
mapResponse.setAttribute('identifier', identifier);
gt.appendChild(mapResponse);

const baseValue = doc.createElementNS(QTI_NAMESPACE, 'qti-base-value');
baseValue.setAttribute('base-type', 'float');
baseValue.textContent = '0';
gt.appendChild(baseValue);

return gt;
}

/**
* Create a feedback condition for a specific choice selection.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ adaptive="false" time-dependent="false" xml:lang="en">

<qti-response-condition>
<qti-response-if>
<qti-match>
<qti-variable identifier="RESPONSE"/>
<qti-correct identifier="RESPONSE"/>
</qti-match>
<qti-gt>
<qti-map-response identifier="RESPONSE"/>
<qti-base-value base-type="float">0</qti-base-value>
</qti-gt>
<qti-set-outcome-value identifier="FEEDBACK">
<qti-multiple>
<qti-variable identifier="FEEDBACK"/>
Expand Down
91 changes: 75 additions & 16 deletions packages/cutie-example/src/example-items/text-entry-multi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0p1_v1p0.xsd"
identifier="text-entry-multi" title="Text Entry - Multiple in Paragraph"
adaptive="false" time-dependent="false" xml:lang="en">

<qti-response-declaration identifier="RESPONSE1" cardinality="single" base-type="string">
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="string">
<qti-correct-response>
<qti-value>1776</qti-value>
</qti-correct-response>
Expand All @@ -19,7 +19,7 @@ adaptive="false" time-dependent="false" xml:lang="en">
</qti-mapping>
</qti-response-declaration>

<qti-response-declaration identifier="RESPONSE2" cardinality="single" base-type="string">
<qti-response-declaration identifier="RESPONSE_2" cardinality="single" base-type="string">
<qti-correct-response>
<qti-value>Philadelphia</qti-value>
</qti-correct-response>
Expand All @@ -29,7 +29,7 @@ adaptive="false" time-dependent="false" xml:lang="en">
</qti-mapping>
</qti-response-declaration>

<qti-response-declaration identifier="RESPONSE3" cardinality="single" base-type="string">
<qti-response-declaration identifier="RESPONSE_3" cardinality="single" base-type="string">
<qti-correct-response>
<qti-value>thirteen</qti-value>
</qti-correct-response>
Expand All @@ -55,38 +55,51 @@ adaptive="false" time-dependent="false" xml:lang="en">
<p>Fill in the blanks to complete the paragraph about American history.</p>
<p>
The Declaration of Independence was signed in the year
<qti-text-entry-interaction response-identifier="RESPONSE1" expected-length="4"/>.
<qti-text-entry-interaction response-identifier="RESPONSE" expected-length="4"/>.
The document was adopted by the Continental Congress meeting in
<qti-text-entry-interaction response-identifier="RESPONSE2" expected-length="12"/>,
<qti-text-entry-interaction response-identifier="RESPONSE_2" expected-length="12"/>,
Pennsylvania. At the time, there were
<qti-text-entry-interaction response-identifier="RESPONSE3" expected-length="8"/>
<qti-text-entry-interaction response-identifier="RESPONSE_3" expected-length="8"/>
colonies that declared their independence from British rule.
</p>

<qti-feedback-block outcome-identifier="FEEDBACK" identifier="RESPONSE_correct" show-hide="show" data-feedback-type="correct">
<p><strong>Excellent!</strong> You have correctly identified the key facts about the Declaration of Independence. This foundational document established the principles of American democracy and marked the birth of a new nation.</p>
<p><strong>Correct!</strong> The Declaration of Independence was signed in 1776.</p>
</qti-feedback-block>

<qti-feedback-block outcome-identifier="FEEDBACK" identifier="RESPONSE_incorrect" show-hide="show" data-feedback-type="incorrect">
<p><strong>Not quite.</strong> Review the timeline of the American Revolution and the founding of the United States. Consider when the Continental Congress met and how many colonies participated in the independence movement.</p>
<p><strong>Incorrect.</strong> Consider the key dates of the American Revolution.</p>
</qti-feedback-block>

<qti-feedback-block outcome-identifier="FEEDBACK" identifier="RESPONSE_2_correct" show-hide="show" data-feedback-type="correct">
<p><strong>Correct!</strong> The Continental Congress met in Philadelphia.</p>
</qti-feedback-block>
<qti-feedback-block outcome-identifier="FEEDBACK" identifier="RESPONSE_2_incorrect" show-hide="show" data-feedback-type="incorrect">
<p><strong>Incorrect.</strong> Think about where the Continental Congress convened in Pennsylvania.</p>
</qti-feedback-block>

<qti-feedback-block outcome-identifier="FEEDBACK" identifier="RESPONSE_3_correct" show-hide="show" data-feedback-type="correct">
<p><strong>Correct!</strong> There were thirteen original colonies.</p>
</qti-feedback-block>
<qti-feedback-block outcome-identifier="FEEDBACK" identifier="RESPONSE_3_incorrect" show-hide="show" data-feedback-type="incorrect">
<p><strong>Incorrect.</strong> Review how many colonies participated in the independence movement.</p>
</qti-feedback-block>
</qti-item-body>

<qti-response-processing>
<qti-set-outcome-value identifier="SCORE">
<qti-sum>
<qti-map-response identifier="RESPONSE1"/>
<qti-map-response identifier="RESPONSE2"/>
<qti-map-response identifier="RESPONSE3"/>
<qti-map-response identifier="RESPONSE"/>
<qti-map-response identifier="RESPONSE_2"/>
<qti-map-response identifier="RESPONSE_3"/>
</qti-sum>
</qti-set-outcome-value>

<qti-response-condition>
<qti-response-if>
<qti-gte>
<qti-variable identifier="SCORE"/>
<qti-variable identifier="MAXSCORE"/>
</qti-gte>
<qti-gt>
<qti-map-response identifier="RESPONSE"/>
<qti-base-value base-type="float">0</qti-base-value>
</qti-gt>
<qti-set-outcome-value identifier="FEEDBACK">
<qti-multiple>
<qti-variable identifier="FEEDBACK"/>
Expand All @@ -103,6 +116,52 @@ adaptive="false" time-dependent="false" xml:lang="en">
</qti-set-outcome-value>
</qti-response-else>
</qti-response-condition>

<qti-response-condition>
<qti-response-if>
<qti-gt>
<qti-map-response identifier="RESPONSE_2"/>
<qti-base-value base-type="float">0</qti-base-value>
</qti-gt>
<qti-set-outcome-value identifier="FEEDBACK">
<qti-multiple>
<qti-variable identifier="FEEDBACK"/>
<qti-base-value base-type="identifier">RESPONSE_2_correct</qti-base-value>
</qti-multiple>
</qti-set-outcome-value>
</qti-response-if>
<qti-response-else>
<qti-set-outcome-value identifier="FEEDBACK">
<qti-multiple>
<qti-variable identifier="FEEDBACK"/>
<qti-base-value base-type="identifier">RESPONSE_2_incorrect</qti-base-value>
</qti-multiple>
</qti-set-outcome-value>
</qti-response-else>
</qti-response-condition>

<qti-response-condition>
<qti-response-if>
<qti-gt>
<qti-map-response identifier="RESPONSE_3"/>
<qti-base-value base-type="float">0</qti-base-value>
</qti-gt>
<qti-set-outcome-value identifier="FEEDBACK">
<qti-multiple>
<qti-variable identifier="FEEDBACK"/>
<qti-base-value base-type="identifier">RESPONSE_3_correct</qti-base-value>
</qti-multiple>
</qti-set-outcome-value>
</qti-response-if>
<qti-response-else>
<qti-set-outcome-value identifier="FEEDBACK">
<qti-multiple>
<qti-variable identifier="FEEDBACK"/>
<qti-base-value base-type="identifier">RESPONSE_3_incorrect</qti-base-value>
</qti-multiple>
</qti-set-outcome-value>
</qti-response-else>
</qti-response-condition>
</qti-response-processing>
</qti-assessment-item>`;

Expand Down
Loading