chore: ai, public course and other ui improvements
This commit is contained in:
+72
-3
@@ -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}
|
||||
|
||||
+59
-18
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user