chore: ai, public course and other ui improvements

This commit is contained in:
Rotimi Best
2026-05-09 06:05:03 +01:00
parent 5717dd8004
commit c31ad63955
318 changed files with 20668 additions and 4788 deletions
+6
View File
@@ -8,7 +8,9 @@ import {
updateLessonSchema,
updateLessonContentSchema,
createExerciseSchema,
createExerciseSectionSchema,
updateExerciseSchema,
updateExerciseSectionSchema,
addQuestionsSchema,
updateQuestionsSchema,
updateCourseLandingPageSchema,
@@ -37,7 +39,9 @@ const teacherTools: ToolRegistry = {
[ToolName.UPDATE_LESSON]: updateLessonSchema,
[ToolName.UPDATE_LESSON_CONTENT]: updateLessonContentSchema,
[ToolName.CREATE_EXERCISE]: createExerciseSchema,
[ToolName.CREATE_EXERCISE_SECTION]: createExerciseSectionSchema,
[ToolName.UPDATE_EXERCISE]: updateExerciseSchema,
[ToolName.UPDATE_EXERCISE_SECTION]: updateExerciseSectionSchema,
[ToolName.ADD_QUESTIONS]: addQuestionsSchema,
[ToolName.UPDATE_QUESTIONS]: updateQuestionsSchema,
[ToolName.UPDATE_COURSE_LANDING_PAGE]: updateCourseLandingPageSchema,
@@ -76,7 +80,9 @@ export {
updateLessonSchema,
updateLessonContentSchema,
createExerciseSchema,
createExerciseSectionSchema,
updateExerciseSchema,
updateExerciseSectionSchema,
addQuestionsSchema,
updateQuestionsSchema,
updateCourseLandingPageSchema,
+67 -7
View File
@@ -1,6 +1,9 @@
import { z } from 'zod';
import { QUESTION_TYPE_REGISTRY } from '@cio/question-types';
import { CoursePlanSchema } from '../types';
const QUESTION_TYPE_ID_DESCRIPTION = QUESTION_TYPE_REGISTRY.map((t) => `${t.id}=${t.typename} (${t.label})`).join(', ');
/**
* Mutation tool schemas available only to teachers.
*/
@@ -15,6 +18,12 @@ export const createSectionSchema = {
})
};
const exerciseSectionAfterBehaviorSchema = z.discriminatedUnion('action', [
z.object({ action: z.literal('continue') }),
z.object({ action: z.literal('submit') }),
z.object({ action: z.literal('go_to_section'), exerciseSectionId: z.string().uuid() })
]);
export const updateSectionSchema = {
description:
'Update metadata for an existing section in the course. Use this to rename a section or change its display order instead of creating a new section.',
@@ -117,9 +126,9 @@ export const createExerciseSchema = {
.number()
.int()
.min(1)
.max(14)
.max(QUESTION_TYPE_REGISTRY.length)
.default(1)
.describe('Question type: 1=RADIO (single answer), 2=CHECKBOX (multiple), 4=TRUE_FALSE, 3=TEXTAREA'),
.describe(`Question type ID. Supported types: ${QUESTION_TYPE_ID_DESCRIPTION}`),
points: z.number().min(0).default(1).describe('Points for this question'),
order: z.number().int().min(0).describe('Display order'),
options: z
@@ -141,6 +150,36 @@ export const createExerciseSchema = {
})
};
export const createExerciseSectionSchema = {
description:
'Create a new section inside an existing exercise. Use this to add a new question block before adding questions into it.',
parameters: z.object({
courseId: z.string().describe('The course ID'),
exerciseId: z.string().describe('The exercise that will contain this section'),
title: z.string().min(1).describe('Section heading shown above questions in this block'),
description: z.string().optional().describe('Optional intro HTML for this block'),
order: z.number().int().min(0).describe('Display order within the exercise (0-based)'),
colorTheme: z.enum(['blue', 'green', 'amber', 'rose', 'violet', 'slate']).optional(),
afterBehavior: exerciseSectionAfterBehaviorSchema.optional()
})
};
export const updateExerciseSectionSchema = {
description:
'Update the title and/or description of one question block inside an exercise (from get_exercise_details sections). Not the same as update_section (course outline).',
parameters: z
.object({
courseId: z.string().describe('The course ID'),
exerciseId: z.string().describe('The exercise containing this block'),
exerciseSectionId: z.string().describe('Section id from get_exercise_details sections[].id'),
title: z.string().min(1).optional().describe('Heading shown above questions in this block'),
description: z.string().optional().describe('Optional intro HTML for this block; empty string clears it')
})
.refine((data) => data.title !== undefined || data.description !== undefined, {
message: 'Provide at least title or description'
})
};
export const updateExerciseSchema = {
description:
"Update an existing exercise's metadata (title, description, linked lesson, section, order, due date, lock state, allow-multiple-attempts). Use this to edit the exercise itself — not its questions. To change questions, use update_questions; to add questions, use add_questions.",
@@ -181,10 +220,17 @@ export const updateExerciseSchema = {
export const addQuestionsSchema = {
description:
'Add questions to an existing exercise. Use this when the exercise already exists and you want to add more questions.',
'Add questions to an existing exercise. Use this when the exercise already exists and you want to add more questions. When get_exercise_details returns a non-empty sections array, pass exerciseSectionId so new questions are placed in the right in-exercise block (same ids as update_exercise_section).',
parameters: z.object({
courseId: z.string().describe('The course ID'),
exerciseId: z.string().describe('The existing exercise ID'),
exerciseSectionId: z
.string()
.uuid()
.optional()
.describe(
'In-exercise section id from get_exercise_details `sections[].id` (not a course outline section id). Use when the exercise has question blocks/sections; omit only when questions are not grouped into sections.'
),
questions: z.array(
z.object({
question: z.string().min(1).describe('The question text'),
@@ -192,9 +238,9 @@ export const addQuestionsSchema = {
.number()
.int()
.min(1)
.max(14)
.max(QUESTION_TYPE_REGISTRY.length)
.default(1)
.describe('Question type: 1=RADIO, 2=CHECKBOX, 4=TRUE_FALSE, 3=TEXTAREA'),
.describe(`Question type ID. Supported types: ${QUESTION_TYPE_ID_DESCRIPTION}`),
points: z.number().min(0).default(1).describe('Points for this question'),
order: z.number().int().min(0).describe('Display order'),
options: z.array(
@@ -210,7 +256,7 @@ export const addQuestionsSchema = {
export const updateQuestionsSchema = {
description:
'Update existing questions in an exercise. Pass only the fields you want to change; `id` identifies the question. For NUMERIC the correct answer is `settings.correctValue` (a number), not an option — do NOT add options to NUMERIC questions. For STAR use `settings.correctValue` (1..max). For WORD_BANK use `settings.correctAnswers` and `settings.template`. RADIO/CHECKBOX/TRUE_FALSE use `options[].isCorrect`: include an option `id` to edit it, omit `id` to add a new one. `settings` is shallow-merged with the existing settings. Omit `options` entirely to leave existing options unchanged — if you pass an options array, any existing option not listed in it will be removed.',
'Update existing questions in an exercise. Pass only the fields you want to change; `id` identifies the question. Optional `exerciseSectionId` (from get_exercise_details sections) moves the question to another in-exercise block; use null to clear section assignment. For NUMERIC the correct answer is `settings.correctValue` (a number), not an option — do NOT add options to NUMERIC questions. For STAR use `settings.correctValue` (1..max). For WORD_BANK use `settings.correctAnswers` and `settings.template`. RADIO/CHECKBOX/TRUE_FALSE use `options[].isCorrect`: include an option `id` to edit it, omit `id` to add a new one. `settings` is shallow-merged with the existing settings. Omit `options` entirely to leave existing options unchanged — if you pass an options array, any existing option not listed in it will be removed.',
parameters: z.object({
courseId: z.string().describe('The course ID'),
exerciseId: z.string().describe('The existing exercise ID'),
@@ -218,9 +264,23 @@ export const updateQuestionsSchema = {
z.object({
id: z.number().int().describe('Existing question id. Required.'),
question: z.string().min(1).optional().describe('New question text. Omit to keep unchanged.'),
questionTypeId: z.number().int().min(1).max(14).optional(),
questionTypeId: z
.number()
.int()
.min(1)
.max(QUESTION_TYPE_REGISTRY.length)
.optional()
.describe(`Question type ID. Supported types: ${QUESTION_TYPE_ID_DESCRIPTION}`),
points: z.number().min(0).optional(),
order: z.number().int().min(0).optional(),
exerciseSectionId: z
.string()
.uuid()
.nullable()
.optional()
.describe(
'In-exercise section id from get_exercise_details (not course outline). Omit to leave unchanged; pass null to unassign from a section.'
),
settings: z
.record(z.string(), z.unknown())
.optional()