* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * chore(mcp): bump version to 0.0.10 Co-authored-by: Rotimi Best <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]>
331 lines
9.9 KiB
TypeScript
331 lines
9.9 KiB
TypeScript
export interface QuestionStoryFixture {
|
|
question: Record<string, unknown>;
|
|
/** Correct answer (`AnswerData` or legacy shapes supported by renderers). */
|
|
answer?: unknown;
|
|
/** Incorrect answer for Review Mode demos in Storybook (`AnswerData`). */
|
|
wrongAnswer?: unknown;
|
|
}
|
|
|
|
export interface ExerciseSectionStoryFixture {
|
|
id: string;
|
|
title: string;
|
|
description?: string;
|
|
order: number;
|
|
colorTheme: 'blue' | 'green' | 'amber' | 'rose' | 'violet' | 'slate';
|
|
questions: Record<string, unknown>[];
|
|
}
|
|
|
|
const OPTION_IMAGE_A = 'https://brand.cdn.clsrio.com/classroomio-lms-courses.png';
|
|
const OPTION_IMAGE_B = 'https://assets.cdn.clsrio.com/www/futuristic-classroom.jpg';
|
|
|
|
export const RADIO_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-radio',
|
|
key: 'q-radio',
|
|
title: 'Which runtime powers most ClassroomIO backend APIs? [RADIO]',
|
|
questionType: 'RADIO',
|
|
settings: {
|
|
description: 'Choose one option.',
|
|
imageUrls: ['https://brand.cdn.clsrio.com/classroomio-courses.png']
|
|
},
|
|
options: [
|
|
{
|
|
id: 1001,
|
|
label: 'Node.js',
|
|
value: 'nodejs',
|
|
isCorrect: true,
|
|
settings: { imageUrl: OPTION_IMAGE_A }
|
|
},
|
|
{
|
|
id: 1002,
|
|
label: 'PHP',
|
|
value: 'php',
|
|
settings: { imageUrl: OPTION_IMAGE_B }
|
|
},
|
|
{ id: 1003, label: 'Ruby', value: 'ruby' }
|
|
]
|
|
},
|
|
answer: { type: 'RADIO', optionId: 1001 },
|
|
wrongAnswer: { type: 'RADIO', optionId: 1002 }
|
|
};
|
|
|
|
export const CHECKBOX_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-checkbox',
|
|
key: 'q-checkbox',
|
|
title: 'Select benefits of TypeScript for an LMS product. [CHECKBOX]',
|
|
questionType: 'CHECKBOX',
|
|
settings: {
|
|
description: 'Select one or more options.'
|
|
},
|
|
options: [
|
|
{
|
|
id: 2001,
|
|
label: 'Better editor autocompletion',
|
|
value: 'autocomplete',
|
|
isCorrect: true,
|
|
settings: { imageUrl: OPTION_IMAGE_A }
|
|
},
|
|
{ id: 2002, label: 'No build tools needed', value: 'no-build-tools' },
|
|
{
|
|
id: 2003,
|
|
label: 'Safer refactoring',
|
|
value: 'safer-refactor',
|
|
isCorrect: true,
|
|
settings: { imageUrl: OPTION_IMAGE_B }
|
|
}
|
|
]
|
|
},
|
|
answer: { type: 'CHECKBOX', optionIds: [2001, 2003] },
|
|
wrongAnswer: { type: 'CHECKBOX', optionIds: [2002] }
|
|
};
|
|
|
|
export const TEXTAREA_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-textarea',
|
|
key: 'q-textarea',
|
|
title: 'Explain why exercise feedback should be specific. [TEXTAREA]',
|
|
questionType: 'TEXTAREA',
|
|
settings: {
|
|
description: 'Provide a detailed response.',
|
|
minCharacters: 30,
|
|
maxCharacters: 250
|
|
}
|
|
},
|
|
answer: '<p>Specific feedback helps students improve on the next attempt quickly.</p>'
|
|
};
|
|
|
|
export const THUMBS_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-thumbs',
|
|
key: 'q-thumbs',
|
|
title: 'Thumbs Up or Down: Would you recommend ClassroomIO to a colleague? [THUMBS]',
|
|
questionType: 'THUMBS',
|
|
options: [
|
|
{ id: 5001, label: 'Yes', value: 'true', isCorrect: false },
|
|
{ id: 5002, label: 'No', value: 'false', isCorrect: false }
|
|
],
|
|
settings: {}
|
|
},
|
|
answer: { type: 'THUMBS', value: true },
|
|
wrongAnswer: { type: 'THUMBS', value: false }
|
|
};
|
|
|
|
export const TRUE_FALSE_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-true-false',
|
|
key: 'q-true-false',
|
|
title: 'True or False: Multiple-choice questions can be auto-graded. [TRUE_FALSE]',
|
|
questionType: 'TRUE_FALSE',
|
|
options: [
|
|
{ id: 4001, label: 'True', value: 'true' },
|
|
{ id: 4002, label: 'False', value: 'false', isCorrect: false }
|
|
],
|
|
settings: {
|
|
correctValue: true
|
|
}
|
|
},
|
|
answer: { type: 'TRUE_FALSE', value: true },
|
|
wrongAnswer: { type: 'TRUE_FALSE', value: false }
|
|
};
|
|
|
|
export const SHORT_ANSWER_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-short-answer',
|
|
key: 'q-short-answer',
|
|
title: 'Name one strategy for reducing flaky frontend tests. [SHORT_ANSWER]',
|
|
questionType: 'SHORT_ANSWER',
|
|
settings: {
|
|
instructions: 'Answer in one sentence.',
|
|
acceptedAnswers: 'use deterministic test fixtures'
|
|
}
|
|
},
|
|
answer: { type: 'SHORT_ANSWER', text: 'Use deterministic test fixtures' },
|
|
wrongAnswer: { type: 'SHORT_ANSWER', text: 'Guess randomly every time' }
|
|
};
|
|
|
|
export const NUMERIC_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-numeric',
|
|
key: 'q-numeric',
|
|
title: 'How many minutes are in a 1.5 hour workshop? [NUMERIC]',
|
|
questionType: 'NUMERIC',
|
|
settings: { correctValue: 90, tolerance: 0 }
|
|
},
|
|
answer: { type: 'NUMERIC', value: 90 },
|
|
wrongAnswer: { type: 'NUMERIC', value: 42 }
|
|
};
|
|
|
|
export const STAR_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-star',
|
|
key: 'q-star',
|
|
title: 'How clear were the lesson objectives? [STAR]',
|
|
questionType: 'STAR',
|
|
settings: { correctValue: 4, maxStars: 5 }
|
|
},
|
|
answer: { type: 'STAR', value: 4 },
|
|
wrongAnswer: { type: 'STAR', value: 2 }
|
|
};
|
|
|
|
export const FILL_BLANK_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-fill-blank',
|
|
key: 'q-fill-blank',
|
|
title: 'An ___ endpoint should be idempotent when using PUT. [FILL_BLANK]',
|
|
questionType: 'FILL_BLANK',
|
|
settings: { acceptedAnswers: 'API,endpoint' }
|
|
},
|
|
answer: { type: 'FILL_BLANK', values: ['API'] },
|
|
wrongAnswer: { type: 'FILL_BLANK', values: ['SOAP'] }
|
|
};
|
|
|
|
export const WORD_BANK_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-word-bank',
|
|
key: 'q-word-bank',
|
|
title: 'Match each term with the sentence. [WORD_BANK]',
|
|
questionType: 'WORD_BANK',
|
|
settings: {
|
|
template:
|
|
'Measurements to describe our data are called ___. The ___ shows values and how often they occur. Outliers appear when data is skewed or has ___.',
|
|
correctAnswers: ['summary statistics', 'distribution', 'outliers'],
|
|
distractors: ['categorical variables', 'linear relationships', 'numeric variables']
|
|
}
|
|
},
|
|
answer: {
|
|
type: 'WORD_BANK',
|
|
filledBlanks: ['summary statistics', 'distribution', 'outliers']
|
|
},
|
|
wrongAnswer: {
|
|
type: 'WORD_BANK',
|
|
filledBlanks: ['numeric variables', 'distribution', 'linear relationships']
|
|
}
|
|
};
|
|
|
|
export const FILE_UPLOAD_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-file-upload',
|
|
key: 'q-file-upload',
|
|
title: 'Upload your lesson plan document. [FILE_UPLOAD]',
|
|
questionType: 'FILE_UPLOAD',
|
|
settings: {
|
|
acceptedTypes: [
|
|
'application/pdf',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'application/msword'
|
|
],
|
|
maxSizeMb: 2
|
|
}
|
|
},
|
|
answer: 'lesson-plan-week-03.pdf'
|
|
};
|
|
|
|
export const VIDEO_RECORDING_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-video-recording',
|
|
key: 'q-video-recording',
|
|
title: 'Record a short reflection on what you learned. [VIDEO_RECORDING]',
|
|
questionType: 'VIDEO_RECORDING',
|
|
settings: {
|
|
description: 'Use the browser camera and microphone. You can retake before submitting.',
|
|
maxDurationSeconds: 120,
|
|
allowRetakes: true
|
|
}
|
|
},
|
|
answer: {
|
|
type: 'VIDEO_RECORDING',
|
|
assetId: '00000000-0000-4000-8000-000000000001',
|
|
storageKey: 'exercise-recordings/story/video-recording-demo.webm',
|
|
fileName: 'video-recording-demo.webm',
|
|
mimeType: 'video/webm',
|
|
size: 1_204_224,
|
|
durationSeconds: 42,
|
|
recordedAt: '2026-04-30T12:00:00.000Z',
|
|
uploadedAt: '2026-04-30T12:01:00.000Z',
|
|
provider: 'cloudflare',
|
|
retakeCount: 2,
|
|
playbackUrl: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm'
|
|
}
|
|
};
|
|
|
|
export const MATCHING_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-matching',
|
|
key: 'q-matching',
|
|
title: 'Match each layer to the ClassroomIO responsibility. [MATCHING]',
|
|
questionType: 'MATCHING',
|
|
settings: {
|
|
pairs: [
|
|
{ left: 'API', right: 'request validation' },
|
|
{ left: 'DB', right: 'persistent storage' }
|
|
]
|
|
}
|
|
},
|
|
answer: { API: 'request validation', DB: 'persistent storage' }
|
|
};
|
|
|
|
export const ORDERING_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-ordering',
|
|
key: 'q-ordering',
|
|
title: 'Order these release steps for a new exercise type. [ORDERING]',
|
|
questionType: 'ORDERING',
|
|
options: [
|
|
{ id: 'order-1', label: 'Design schema', value: 'Design schema' },
|
|
{ id: 'order-2', label: 'Implement renderer', value: 'Implement renderer' },
|
|
{ id: 'order-3', label: 'Ship to production', value: 'Ship to production' }
|
|
],
|
|
settings: {
|
|
items: ['Design schema', 'Implement renderer', 'Ship to production']
|
|
}
|
|
},
|
|
answer: { type: 'ORDERING', orderedValues: ['order-1', 'order-2', 'order-3'] },
|
|
wrongAnswer: { type: 'ORDERING', orderedValues: ['order-3', 'order-1', 'order-2'] }
|
|
};
|
|
|
|
export const HOTSPOT_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-hotspot',
|
|
key: 'q-hotspot',
|
|
title: 'Mark where the submit button should appear in the layout. [HOTSPOT]',
|
|
questionType: 'HOTSPOT',
|
|
settings: {
|
|
hotspots: [{ x: 46, y: 68 }]
|
|
}
|
|
},
|
|
answer: { points: [{ x: 46, y: 68 }] }
|
|
};
|
|
|
|
export const LINK_FIXTURE: QuestionStoryFixture = {
|
|
question: {
|
|
id: 'q-link',
|
|
key: 'q-link',
|
|
title: 'Share the resources you used while solving this exercise. [LINK]',
|
|
questionType: 'LINK',
|
|
settings: {
|
|
instructions: 'Add one or more relevant links.'
|
|
}
|
|
},
|
|
answer: ['https://classroomio.com/docs', 'https://www.svelte.dev']
|
|
};
|
|
|
|
export const SECTIONED_EXERCISE_FIXTURE: ExerciseSectionStoryFixture[] = [
|
|
{
|
|
id: 'section-foundations',
|
|
title: 'Foundations',
|
|
description: 'A short warm-up section before the applied scenario.',
|
|
order: 0,
|
|
colorTheme: 'blue',
|
|
questions: [RADIO_FIXTURE.question, TRUE_FALSE_FIXTURE.question]
|
|
},
|
|
{
|
|
id: 'section-application',
|
|
title: 'Applied Practice',
|
|
description: 'Students solve open-response and ordering tasks.',
|
|
order: 1,
|
|
colorTheme: 'green',
|
|
questions: [SHORT_ANSWER_FIXTURE.question, ORDERING_FIXTURE.question]
|
|
}
|
|
];
|