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
@@ -4,13 +4,14 @@
import { UserAvatar } from '@cio/ui/custom/user-avatar';
import { submissions } from './store';
import { questionnaire } from '../store';
import { getQuestionsForSection, questionnaire } from '../store';
import { t } from '$lib/utils/functions/translations';
import type { ExerciseSubmissions } from './types';
import type { Question } from '$features/course/types';
import type { AnswerData } from '@cio/question-types';
import { toExerciseQuestionModel } from '../question-type-utils';
import { getExerciseQuestionLabels } from '../question-labels';
import { getExerciseSectionDisplayTitle } from '$features/course/utils/exercise-section-utils';
interface Props {
isLoading?: boolean;
@@ -20,6 +21,11 @@
let studentSelected = $state(0);
const questionLabels = $derived(getExerciseQuestionLabels());
const sectionFallbackTitle = $derived($t('course.navItem.lessons.exercises.all_exercises.section.fallback_title'));
const activeSections = $derived(
[...($questionnaire.sections ?? [])].filter((section) => !section.deletedAt).sort((a, b) => a.order - b.order)
);
const hasSectionGroups = $derived(activeSections.length > 0);
function toNumericQuestionId(question: Question): number | null {
const numericId = Number(question.id);
@@ -42,6 +48,20 @@
return submittedAnswer.answerData;
}
function getSectionScore(student: ExerciseSubmissions, questions: Question[]) {
return questions.reduce((total, question) => {
const questionId = toNumericQuestionId(question);
if (questionId === null) return total;
const answer = student.answers.find((item) => item.questionId === questionId);
return total + (answer?.point ?? 0);
}, 0);
}
function getSectionMaxPoints(questions: Question[]) {
return questions.reduce((total, question) => total + Number(question.points ?? 0), 0);
}
</script>
{#if isLoading}
@@ -74,17 +94,66 @@
)}
</p>
{#if $submissions[studentSelected]}
{#if $questionnaire.questions}
{#if hasSectionGroups}
<div class="space-y-8">
{#each activeSections as section, sectionIndex (section.id)}
{@const sectionQuestions = getQuestionsForSection($questionnaire.questions, section.id)}
<section class="space-y-4">
<ExerciseQuestion.SectionHeader
title={getExerciseSectionDisplayTitle({
title: section.title,
sectionNumber: sectionIndex + 1,
sectionLabel: sectionFallbackTitle
})}
description={section.description}
sectionNumber={sectionIndex + 1}
totalSections={activeSections.length}
colorTheme={section.colorTheme}
questionCount={sectionQuestions.length}
totalPoints={getSectionMaxPoints(sectionQuestions)}
labels={{
section: $t('course.navItem.lessons.exercises.all_exercises.section.fallback_title'),
questions: $t('course.navItem.lessons.exercises.all_exercises.view_mode.questions'),
points: $t('course.navItem.lessons.exercises.all_exercises.view_mode.points')
}}
/>
{#each sectionQuestions as q, i (`${q.id}-${i}`)}
<div class="pb-4">
<ExerciseQuestion.QuestionRenderer
contract={{
mode: 'review',
question: toExerciseQuestionModel(q),
answer: getStudentAnswerForQuestion($submissions[studentSelected], q),
labels: questionLabels,
disabled: true
}}
questionNumber={i + 1}
questionNumberActive={false}
/>
</div>
{/each}
<p class="border-t pt-2 text-sm font-medium">
{getSectionScore($submissions[studentSelected], sectionQuestions)}/{getSectionMaxPoints(sectionQuestions)}
{$t('course.navItem.lessons.exercises.all_exercises.view_mode.points')}
</p>
</section>
{/each}
</div>
{:else if $questionnaire.questions}
{#each $questionnaire.questions as q, i (`${q.id}-${i}`)}
<div class="pb-4">
<ExerciseQuestion.QuestionRenderer
contract={{
mode: 'review',
question: { ...toExerciseQuestionModel(q), title: `${i + 1}. ${q.title}` },
question: toExerciseQuestionModel(q),
answer: getStudentAnswerForQuestion($submissions[studentSelected], q),
labels: questionLabels,
disabled: true
}}
questionNumber={i + 1}
questionNumberActive={false}
/>
</div>
{/each}
@@ -4,18 +4,20 @@
import { ExerciseQuestion } from '@cio/ui';
import { submissions } from './store';
import { questionnaire } from '../store';
import { getQuestionsForSection, questionnaire } from '../store';
import { t } from '$lib/utils/functions/translations';
import type { Question } from '$features/course/types';
import type { ExerciseSubmissions } from './types';
import type { AnswerData, ExerciseQuestionLabels, ExerciseSubmissionModel } from '@cio/question-types';
import { toExerciseQuestionModel } from '../question-type-utils';
import { getExerciseSectionDisplayTitle } from '$features/course/utils/exercise-section-utils';
interface Props {
isLoading?: boolean;
}
let { isLoading = $bindable(true) }: Props = $props();
const sectionFallbackTitle = $derived($t('course.navItem.lessons.exercises.all_exercises.section.fallback_title'));
function toAnswerData(answerData: unknown): AnswerData | null {
if (!answerData || typeof answerData !== 'object' || !('type' in answerData)) return null;
@@ -34,13 +36,6 @@
};
}
function toSubmissionQuestionModel(question: Question, index: number) {
return {
...toExerciseQuestionModel(question),
title: `${index + 1}. ${question.title || ''}`
};
}
const submissionLabels = $derived.by(
(): ExerciseQuestionLabels => ({
'submission.common.no_answer': $t('course.navItem.lessons.exercises.all_exercises.analytics.individual.no'),
@@ -59,8 +54,16 @@
const submissionQuestions = $derived(
$questionnaire.questions
.filter((question) => !question.deletedAt)
.map((question, index) => toSubmissionQuestionModel(question, index))
.map((question) => toExerciseQuestionModel(question))
);
const activeSections = $derived(
[...($questionnaire.sections ?? [])].filter((section) => !section.deletedAt).sort((a, b) => a.order - b.order)
);
const hasSectionGroups = $derived(activeSections.length > 0);
function getSectionPoints(questions: Question[]) {
return questions.reduce((total, question) => total + Number(question.points ?? 0), 0);
}
</script>
{#if isLoading}
@@ -71,14 +74,52 @@
{$t('course.navItem.lessons.exercises.all_exercises.analytics.summary.question_chart')}
</p>
<ExerciseQuestion.QuestionList
contract={{
mode: 'submission',
questions: submissionQuestions,
submissions: submissionModels,
labels: submissionLabels,
disabled: true
}}
/>
{#if hasSectionGroups}
<div class="space-y-8">
{#each activeSections as section, sectionIndex (section.id)}
{@const sectionQuestions = getQuestionsForSection($questionnaire.questions, section.id)}
<section class="space-y-4">
<ExerciseQuestion.SectionHeader
title={getExerciseSectionDisplayTitle({
title: section.title,
sectionNumber: sectionIndex + 1,
sectionLabel: sectionFallbackTitle
})}
description={section.description}
sectionNumber={sectionIndex + 1}
totalSections={activeSections.length}
colorTheme={section.colorTheme}
questionCount={sectionQuestions.length}
totalPoints={getSectionPoints(sectionQuestions)}
labels={{
section: $t('course.navItem.lessons.exercises.all_exercises.section.fallback_title'),
questions: $t('course.navItem.lessons.exercises.all_exercises.view_mode.questions'),
points: $t('course.navItem.lessons.exercises.all_exercises.view_mode.points')
}}
/>
<ExerciseQuestion.QuestionList
contract={{
mode: 'submission',
questions: sectionQuestions.map((question) => toExerciseQuestionModel(question)),
submissions: submissionModels,
labels: submissionLabels,
disabled: true
}}
/>
</section>
{/each}
</div>
{:else}
<ExerciseQuestion.QuestionList
contract={{
mode: 'submission',
questions: submissionQuestions,
submissions: submissionModels,
labels: submissionLabels,
disabled: true
}}
/>
{/if}
</div>
{/if}