Skip to content

Commit d5f71a4

Browse files
committed
fix: align mc-populated-blank with CQT variant behavior
Bring delivery/controller behavior and styling closer to CQT across variants, including VIC/SEL VIC parity and demo sample naming updates used by product stakeholders. Made-with: Cursor
1 parent 2e1338c commit d5f71a4

10 files changed

Lines changed: 431 additions & 142 deletions

File tree

apps/element-demo/src/lib/samples/mc-populated-blank.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@
5151
},
5252
{
5353
"id": "variant-sr-vic",
54-
"title": "sr-vic (canonical CQT sample)",
54+
"title": "VIC (canonical CQT sample)",
5555
"description": "Canonical sample from item 00293f60-334f-48d0-b145-f7cb9f02a0fe and question a546c0a3-3d68-4d5c-994f-262e788f486c.",
56-
"tags": ["mc-populated-blank", "cqt-sample", "sr-vic", "sr-vicv0.0.1"],
56+
"tags": ["mc-populated-blank", "cqt-sample", "VIC", "sr-vic", "sr-vicv0.0.1"],
5757
"model": {
5858
"id": "2",
5959
"element": "mc-populated-blank",
@@ -104,9 +104,9 @@
104104
},
105105
{
106106
"id": "variant-sel-vic",
107-
"title": "sel_vic (canonical CQT sample)",
107+
"title": "SEL VIC (canonical CQT sample)",
108108
"description": "Canonical sample from item 04973d1a-0557-4963-937d-7ac76ee3baeb and question 11e9862e-cab1-415e-87a3-0f32fedcedfc.",
109-
"tags": ["mc-populated-blank", "cqt-sample", "sel_vic", "sel_vicv0.0.1"],
109+
"tags": ["mc-populated-blank", "cqt-sample", "SEL VIC", "sel_vic", "sel_vicv0.0.1"],
110110
"model": {
111111
"id": "3",
112112
"element": "mc-populated-blank",

bun.lock

Lines changed: 4 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# mc-populated-blank Contract Checklist
2+
3+
This checklist defines the required PIE contract behavior for `@pie-element/mc-populated-blank`.
4+
5+
## Model output (`controller.model`)
6+
7+
- [ ] `mode` is present and equals `env.mode`.
8+
- [ ] `disabled` is `true` when `mode !== "gather"`.
9+
- [ ] `correctChoiceId` is only present in `evaluate` mode.
10+
- [ ] `responseCorrect` is only present in `evaluate` mode.
11+
- [ ] `teacherInstructions` is only present for instructor `view`/`evaluate`.
12+
- [ ] `choices` order is deterministic when shuffling is enabled and not locked.
13+
14+
## Outcome output (`controller.outcome`)
15+
16+
- [ ] Returns a consistent shape with `score`, `empty`, and `traceLog`.
17+
- [ ] Empty or unanswered sessions return `score: 0` and `empty: true`.
18+
- [ ] Answered sessions return `empty: false` and binary score (`0` or `1`).
19+
20+
## Completion semantics
21+
22+
- [ ] `isComplete` is the single source of truth for completion.
23+
- [ ] Completion respects audio gating when autoplay + complete-audio are enabled.
24+
- [ ] Delivery does not emit divergent completeness metadata.
25+
26+
## Event contract (`delivery`)
27+
28+
- [ ] `model-set` and `session-changed` details use `component: tagName.toLowerCase()`.
29+
- [ ] `session-changed` detail `complete` comes from controller `isComplete`.
30+
- [ ] No fallback legacy event payloads are emitted.
31+
32+
## Packaging contract
33+
34+
- [ ] `exports` entries resolve to built files for `.`, `./delivery`, `./controller`, `./author`, `./print`.
35+
- [ ] IIFE policy is explicit via `./iife` export.
36+
- [ ] Runtime dependencies include only what delivery/controller actually import.

packages/elements-svelte/mc-populated-blank/package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
"development": "./src/print/index.ts",
3131
"types": "./dist/print/index.d.ts",
3232
"default": "./dist/print/index.js"
33+
},
34+
"./iife": {
35+
"default": "./dist/index.iife.js"
3336
}
3437
},
3538
"files": [
@@ -43,18 +46,13 @@
4346
"clean": "rm -rf dist tsconfig.tsbuildinfo"
4447
},
4548
"dependencies": {
46-
"@pie-lib/editable-html-tiptap-svelte": "workspace:*",
47-
"@pie-lib-svelte/styling": "workspace:*",
4849
"lodash-es": "^4.17.21"
4950
},
5051
"peerDependencies": {
5152
"svelte": "^5.0.0"
5253
},
5354
"devDependencies": {
5455
"@sveltejs/vite-plugin-svelte": "^7.0.0",
55-
"@tiptap/core": "^3.19.0",
56-
"@tiptap/pm": "^3.19.0",
57-
"@tiptap/starter-kit": "^3.19.0",
5856
"@types/lodash-es": "^4.17.12",
5957
"svelte": "^5.54.0",
6058
"typescript": "^5.9.3",

packages/elements-svelte/mc-populated-blank/src/controller/index.ts

Lines changed: 127 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -42,78 +42,154 @@ export const isComplete = (question: any, session: any, audioComplete = false) =
4242
export const outcome = (question: any, session: any, env: any) =>
4343
new Promise((resolve) => {
4444
if (!session || isEmpty(session)) {
45-
resolve({ score: 0, empty: true });
45+
resolve({
46+
score: 0,
47+
empty: true,
48+
traceLog: ['Student did not select any answers. Score is 0.'],
49+
});
4650
return;
4751
}
4852

4953
session = normalizeSession(session);
54+
const correctness = getCorrectness(question, session);
5055

51-
if (env.mode !== 'evaluate') {
52-
resolve({ score: undefined, completed: undefined });
53-
} else {
54-
const correctness = getCorrectness(question, session);
55-
if (correctness === 'unanswered') {
56-
resolve({ score: 0, empty: true });
57-
return;
58-
}
59-
const score = correctness === 'correct' ? 1 : 0;
60-
resolve({ score, empty: false });
56+
if (correctness === 'unanswered') {
57+
resolve({
58+
score: 0,
59+
empty: true,
60+
traceLog: ['Student did not select any answers. Score is 0.'],
61+
});
62+
return;
6163
}
64+
65+
const score = correctness === 'correct' ? 1 : 0;
66+
const traceLog = [
67+
`Mode: ${env?.mode || 'unknown'}.`,
68+
`Student selected choice: ${session.choiceId}.`,
69+
`Correct choice: ${question?.correctChoiceId || 'none'}.`,
70+
`Final score: ${score}.`,
71+
];
72+
resolve({ score, empty: false, traceLog });
6273
});
6374

6475
export const createDefaultModel = (model: any = {}) => ({ ...defaults.model, ...model });
6576

6677
export const normalizeSession = (s: any) => ({ ...s });
6778

68-
export const model = (question: any, session: any, env: any) => {
69-
return new Promise((resolve) => {
70-
session = session || {};
71-
const normalizedQuestion = createDefaultModel(question);
72-
73-
const out: any = {
74-
prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,
75-
interactionMode: normalizedQuestion.interactionMode || 'populate_blank',
76-
layoutProfile: normalizedQuestion.layoutProfile || '',
77-
choiceLayout: normalizedQuestion.choiceLayout || '',
78-
sentenceHtml: normalizedQuestion.sentenceHtml || null,
79-
template: normalizedQuestion.template,
80-
choiceMode: normalizedQuestion.choiceMode,
81-
choices: normalizedQuestion.choices,
82-
correctChoiceId: normalizedQuestion.correctChoiceId,
83-
hasAudio: normalizedQuestion.hasAudio,
84-
autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,
85-
completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,
86-
audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,
87-
audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,
88-
showVisibleTranscript: !!normalizedQuestion.showVisibleTranscript,
89-
locale: normalizedQuestion.locale || '',
90-
disabled: env.mode !== 'gather',
91-
view: env.mode === 'view',
92-
env,
93-
};
94-
95-
if (env.mode === 'evaluate') {
96-
const correctness = getCorrectness(normalizedQuestion, session);
97-
out.correctness = correctness;
98-
}
79+
const shouldShuffleChoices = (question: any) => !!question?.shuffle;
9980

100-
if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {
101-
out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled
102-
? normalizedQuestion.teacherInstructions
103-
: null;
104-
} else {
105-
out.teacherInstructions = null;
81+
const shouldLockChoices = (question: any, env: any) => {
82+
if (question?.lockChoiceOrder) return true;
83+
if (env?.['@pie-element']?.lockChoiceOrder) return true;
84+
return env?.role === 'instructor';
85+
};
86+
87+
const shuffleArray = <T>(items: T[]): T[] => {
88+
const out = [...items];
89+
for (let i = out.length - 1; i > 0; i--) {
90+
const j = Math.floor(Math.random() * (i + 1));
91+
[out[i], out[j]] = [out[j], out[i]];
92+
}
93+
return out;
94+
};
95+
96+
const getStoredShuffle = (session: any): string[] =>
97+
Array.isArray(session?.data?.shuffledValues)
98+
? session.data.shuffledValues
99+
: Array.isArray(session?.shuffledValues)
100+
? session.shuffledValues
101+
: [];
102+
103+
const applyShuffledValues = (choices: any[], shuffledValues: string[], choiceKey: string) => {
104+
const orderedChoices = shuffledValues
105+
.map((value) => choices.find((choice) => choice?.[choiceKey] === value))
106+
.filter(Boolean);
107+
108+
if (orderedChoices.length === choices.length) {
109+
return orderedChoices;
110+
}
111+
112+
const orderedValues = new Set(orderedChoices.map((choice: any) => choice[choiceKey]));
113+
const leftovers = choices.filter((choice) => !orderedValues.has(choice?.[choiceKey]));
114+
return [...orderedChoices, ...leftovers];
115+
};
116+
117+
const getOrderedChoices = async (question: any, session: any, env: any, updateSession?: any) => {
118+
const choices = Array.isArray(question?.choices) ? [...question.choices] : [];
119+
if (!choices.length || !shouldShuffleChoices(question)) {
120+
return choices;
121+
}
122+
123+
if (shouldLockChoices(question, env || {})) {
124+
return choices;
125+
}
126+
127+
const shuffledValues = getStoredShuffle(session);
128+
if (shuffledValues.length) {
129+
return applyShuffledValues(choices, shuffledValues, 'id');
130+
}
131+
132+
const shuffledChoices = shuffleArray(choices);
133+
134+
if (updateSession && typeof updateSession === 'function' && session?.id && session?.element) {
135+
const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);
136+
if (shuffledIds.length) {
137+
await updateSession(session.id, session.element, { shuffledValues: shuffledIds });
106138
}
139+
}
107140

108-
resolve(out);
109-
});
141+
return shuffledChoices;
142+
};
143+
144+
export const model = async (question: any, session: any, env: any, updateSession?: any) => {
145+
session = session || {};
146+
const normalizedQuestion = createDefaultModel(question);
147+
const choices = await getOrderedChoices(normalizedQuestion, session, env, updateSession);
148+
149+
const out: any = {
150+
prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,
151+
interactionMode: normalizedQuestion.interactionMode || 'populate_blank',
152+
layoutProfile: normalizedQuestion.layoutProfile || '',
153+
choiceLayout: normalizedQuestion.choiceLayout || '',
154+
sentenceHtml: normalizedQuestion.sentenceHtml || null,
155+
template: normalizedQuestion.template,
156+
choiceMode: normalizedQuestion.choiceMode,
157+
choices,
158+
hasAudio: normalizedQuestion.hasAudio,
159+
autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,
160+
completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,
161+
audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,
162+
audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,
163+
showVisibleTranscript: !!normalizedQuestion.showVisibleTranscript,
164+
locale: normalizedQuestion.locale || '',
165+
disabled: env.mode !== 'gather',
166+
view: env.mode === 'view',
167+
mode: env.mode,
168+
};
169+
170+
if (env.mode === 'evaluate') {
171+
const correctness = getCorrectness(normalizedQuestion, session);
172+
out.correctness = correctness;
173+
out.responseCorrect = correctness === 'correct';
174+
out.correctChoiceId = normalizedQuestion.correctChoiceId;
175+
}
176+
177+
if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {
178+
out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled
179+
? normalizedQuestion.teacherInstructions
180+
: null;
181+
} else {
182+
out.teacherInstructions = null;
183+
}
184+
185+
return out;
110186
};
111187

112188
export const createCorrectResponseSession = (question: any, env: any) => {
113189
return new Promise((resolve) => {
114190
if (env.mode !== 'evaluate' && env.role === 'instructor') {
115191
resolve({
116-
id: question?.id || '1',
192+
id: '1',
117193
element: 'mc-populated-blank',
118194
choiceId: question?.correctChoiceId || '',
119195
});

0 commit comments

Comments
 (0)