Skip to content

Commit

Permalink
feat: add evaluation view for asynchronous activities (#4265)
Browse files Browse the repository at this point in the history
  • Loading branch information
sjschlapbach authored Sep 19, 2024
1 parent 533be1b commit 2ea3b69
Show file tree
Hide file tree
Showing 56 changed files with 7,137 additions and 1,939 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import CopyConfirmationToast from '../toasts/CopyConfirmationToast'
import { getAccessLink, getLTIAccessLink } from './PracticeQuizElement'
import StatusTag from './StatusTag'
import MicroLearningAccessLink from './actions/MicroLearningAccessLink'
import MicroLearningEvaluationLink from './actions/MicroLearningEvaluationLink'
import MicroLearningPreviewLink from './actions/MicroLearningPreviewLink'
import PublishMicroLearningButton from './actions/PublishMicroLearningButton'
import getActivityDuplicationAction from './actions/getActivityDuplicationAction'
Expand Down Expand Up @@ -62,6 +63,7 @@ function MicroLearningElement({
})

const href = `${process.env.NEXT_PUBLIC_PWA_URL}/microlearning/${microLearning.id}/`
const evaluationHref = `/microLearning/${microLearning.id}/evaluation`
const isFuture = dayjs(microLearning.scheduledStartAt).isAfter(dayjs())
const isPast = dayjs(microLearning.scheduledEndAt).isBefore(dayjs())

Expand Down Expand Up @@ -252,6 +254,15 @@ function MicroLearningElement({
),
onClick: () => null,
},
{
label: (
<MicroLearningEvaluationLink
quizName={microLearning.name}
evaluationHref={evaluationHref}
/>
),
onClick: () => null,
},
getActivityDuplicationAction({
id: microLearning.id,
text: t('manage.course.duplicateMicroLearning'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { WizardMode } from '../sessions/creation/ElementCreation'
import CopyConfirmationToast from '../toasts/CopyConfirmationToast'
import StatusTag from './StatusTag'
import PracticeQuizAccessLink from './actions/PracticeQuizAccessLink'
import PracticeQuizEvaluationLink from './actions/PracticeQuizEvaluationLink'
import PracticeQuizPreviewLink from './actions/PracticeQuizPreviewLink'
import PublishPracticeQuizButton from './actions/PublishPracticeQuizButton'
import getActivityDuplicationAction from './actions/getActivityDuplicationAction'
Expand Down Expand Up @@ -138,6 +139,7 @@ function PracticeQuizElement({
})

const href = `${process.env.NEXT_PUBLIC_PWA_URL}/course/${courseId}/quiz/${practiceQuiz.id}/`
const evaluationHref = `/practiceQuiz/${practiceQuiz.id}/evaluation`

const statusMap: Record<PublicationStatus, React.ReactElement> = {
[PublicationStatus.Draft]: (
Expand Down Expand Up @@ -352,6 +354,15 @@ function PracticeQuizElement({
),
onClick: () => null,
},
{
label: (
<PracticeQuizEvaluationLink
quizName={practiceQuiz.name}
evaluationHref={evaluationHref}
/>
),
onClick: () => null,
},
getActivityDuplicationAction({
id: practiceQuiz.id,
text: t('manage.course.duplicatePracticeQuiz'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { faExternalLink } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useTranslations } from 'next-intl'
import Link from 'next/link'

interface MicroLearningEvaluationLinkProps {
quizName: string
evaluationHref: string
}

function MicroLearningEvaluationLink({
quizName,
evaluationHref,
}: MicroLearningEvaluationLinkProps) {
const t = useTranslations()

return (
<Link
href={evaluationHref}
target="_blank"
className="text-primary-100 flex flex-row items-center gap-1"
data-cy={`evaluation-microlearning-${quizName}`}
>
<FontAwesomeIcon icon={faExternalLink} size="sm" className="w-4" />
<div>{t('manage.courseList.openEvaluation')}</div>
</Link>
)
}

export default MicroLearningEvaluationLink
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function MicroLearningPreviewLink({
data-cy={`open-microlearning-${microLearning.name}`}
>
<FontAwesomeIcon icon={faExternalLink} size="sm" className="w-4" />
<div>{t('shared.generic.open')}</div>
<div>{t('manage.courseList.openPreview')}</div>
</Link>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { faExternalLink } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useTranslations } from 'next-intl'
import Link from 'next/link'

interface PracticeQuizEvaluationLinkProps {
quizName: string
evaluationHref: string
}

function PracticeQuizEvaluationLink({
quizName,
evaluationHref,
}: PracticeQuizEvaluationLinkProps) {
const t = useTranslations()

return (
<Link
href={evaluationHref}
target="_blank"
className="text-primary-100 flex flex-row items-center gap-1"
data-cy={`evaluation-practice-quiz-${quizName}`}
>
<FontAwesomeIcon icon={faExternalLink} size="sm" className="w-4" />
<div>{t('manage.courseList.openEvaluation')}</div>
</Link>
)
}

export default PracticeQuizEvaluationLink
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function PracticeQuizPreviewLink({
data-cy={`open-practice-quiz-${practiceQuiz.name}`}
>
<FontAwesomeIcon icon={faExternalLink} size="sm" className="w-4" />
<div>{t('shared.generic.open')}</div>
<div>{t('manage.courseList.openPreview')}</div>
</Link>
)
}
Expand Down
177 changes: 177 additions & 0 deletions apps/frontend-manage/src/components/evaluation/ActivityEvaluation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import {
sizeReducer,
TextSizes,
} from '@components/sessions/evaluation/constants'
import { StackEvaluation } from '@klicker-uzh/graphql/dist/ops'
import { ChartType } from '@klicker-uzh/shared-components/src/constants'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useReducer, useState } from 'react'
import { twMerge } from 'tailwind-merge'
import ElementEvaluation from './ElementEvaluation'
import EvaluationFooter from './EvaluationFooter'
import useChartTypeUpdate from './hooks/useChartTypeUpdate'
import useStackInstanceMap from './hooks/useStackInstanceMap'
import EvaluationNavigation from './navigation/EvaluationNavigation'

interface ActivityEvaluationProps {
activityName: string
stacks: StackEvaluation[]
}

export type ActiveStackType = number | 'feedbacks' | 'confusion' | 'leaderboard'

function ActivityEvaluation({ activityName, stacks }: ActivityEvaluationProps) {
const router = useRouter()
const [activeStack, setActiveStack] = useState<ActiveStackType>(0)
const [activeInstance, setActiveInstance] = useState<number>(0)
const [showSolution, setShowSolution] = useState<boolean>(false)
const [chartType, setChartType] = useState<ChartType>(ChartType.UNSET)
const [textSize, setTextSize] = useReducer(sizeReducer, TextSizes['md'])

const instanceResults = stacks.flatMap((stack) => stack.instances)

// compute a map between stack and instance indices {stackIx: [instanceIx1, instanceIx2], ...}
const stackInstanceMap = useStackInstanceMap({ stacks })

// update the chart type as soon as the active instance changes
useChartTypeUpdate({
activeInstance,
activeElementType: instanceResults[activeInstance].type,
chartType,
setChartType,
})

return (
<>
<Head>
<title>{`KlickerUZH - Evaluation: ${activityName}`}</title>
<meta
name="description"
content={`KlickerUZH - Evaluation: ${activityName}`}
charSet="utf-8"
></meta>
</Head>

{router.query.hideControls !== 'true' && (
<div className="z-20 h-11 flex-none">
<EvaluationNavigation
stacks={stacks}
stackInstanceMap={stackInstanceMap}
activeStack={activeStack}
setActiveStack={setActiveStack}
activeInstance={activeInstance}
setActiveInstance={setActiveInstance}
numOfInstances={instanceResults.length}
/>
</div>
)}

<div className="flex min-h-0 flex-1 flex-col">
{typeof activeStack === 'number' && (
<ElementEvaluation
currentInstance={instanceResults[activeInstance]}
textSize={textSize}
chartType={chartType}
showSolution={
instanceResults[activeInstance].hasSampleSolution
? showSolution
: false
}
/>
)}

{/* {showLeaderboard && !showConfusion && !showFeedbacks && (
<div className="overflow-y-auto">
<div className="border-t p-4">
<div className="mx-auto max-w-2xl text-xl">
{data.sessionLeaderboard &&
data.sessionLeaderboard.length > 0 ? (
<Leaderboard
leaderboard={data.sessionLeaderboard ?? []}
podiumImgSrc={{
rank1: Rank1Img,
rank2: Rank2Img,
rank3: Rank3Img,
}}
/>
) : (
<UserNotification
className={{ message: 'text-lg' }}
type="warning"
message={t('manage.evaluation.noSignedInStudents')}
/>
)}
</div>
</div>
</div>
)} */}

{/* {!showLeaderboard &&
!showConfusion &&
showFeedbacks &&
data.sessionEvaluation && (
<div className="overflow-y-auto print:overflow-y-visible">
<div className="p-4">
<div className="mx-auto max-w-5xl text-xl">
{feedbacks && feedbacks.length > 0 ? (
<EvaluationFeedbacks
feedbacks={feedbacks}
sessionName={data.sessionEvaluation.displayName}
/>
) : (
<UserNotification
className={{ message: 'text-lg' }}
type="warning"
message={t('manage.evaluation.noFeedbacksYet')}
/>
)}
</div>
</div>
</div>
)} */}

{/* {!showLeaderboard && showConfusion && !showFeedbacks && (
<div className="overflow-y-auto">
<div className="border-t p-4">
<div className="mx-auto max-w-5xl text-xl">
{confusionFeedbacks && confusionFeedbacks.length > 0 ? (
<EvaluationConfusion confusionTS={confusionFeedbacks} />
) : (
<UserNotification
className={{ message: 'text-lg' }}
type="warning"
message={t('manage.evaluation.noConfusionFeedbacksYet')}
/>
)}
</div>
</div>
</div>
)} */}
</div>

<div
className={twMerge(
'z-20 h-14 flex-none',
(activeStack === 'feedbacks' ||
activeStack === 'confusion' ||
activeStack === 'leaderboard') &&
'h-18'
)}
>
<EvaluationFooter
activeStack={activeStack}
textSize={textSize}
setTextSize={setTextSize}
showSolution={showSolution}
setShowSolution={setShowSolution}
chartType={chartType}
setChartType={setChartType}
currentInstance={instanceResults[activeInstance]}
/>
</div>
</>
)
}

export default ActivityEvaluation
66 changes: 66 additions & 0 deletions apps/frontend-manage/src/components/evaluation/ElementChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
ElementInstanceEvaluation,
NumericalElementInstanceEvaluation,
} from '@klicker-uzh/graphql/dist/ops'
import { ChartType } from '@klicker-uzh/shared-components/src/constants'
import { useTranslations } from 'next-intl'
import React from 'react'
import { TextSizeType } from '../sessions/evaluation/constants'
import ElementBarChart from './charts/ElementBarChart'
import ElementHistogram from './charts/ElementHistogram'
import ElementTableChart from './charts/ElementTableChart'
import ElementWordcloud from './charts/ElementWordcloud'

interface ElementChartProps {
chartType: string
instanceEvaluation: ElementInstanceEvaluation
showSolution: boolean
textSize: TextSizeType
}

function ElementChart({
chartType,
instanceEvaluation,
showSolution,
textSize,
}: ElementChartProps): React.ReactElement {
const t = useTranslations()

if (chartType === ChartType.TABLE) {
return (
<ElementTableChart
instance={instanceEvaluation}
showSolution={showSolution}
textSize={textSize.textLg}
/>
)
} else if (chartType === ChartType.HISTOGRAM) {
return (
<ElementHistogram
instance={instanceEvaluation as NumericalElementInstanceEvaluation}
showSolution={{ general: showSolution }}
textSize={textSize.text}
/>
)
} else if (chartType === ChartType.WORD_CLOUD) {
return (
<ElementWordcloud
instance={instanceEvaluation}
showSolution={showSolution}
textSize={{ min: textSize.min, max: textSize.max }}
/>
)
} else if (chartType === ChartType.BAR_CHART) {
return (
<ElementBarChart
instance={instanceEvaluation}
showSolution={showSolution}
textSize={textSize}
/>
)
} else {
return <div>{t('manage.evaluation.noChartsAvailable')}</div>
}
}

export default ElementChart
Loading

0 comments on commit 2ea3b69

Please sign in to comment.