diff --git a/src/components/ActivityTypes/closeEndedQuestion.tsx b/src/components/ActivityTypes/closeEndedQuestion.tsx index 8d43eff..bb708ef 100644 --- a/src/components/ActivityTypes/closeEndedQuestion.tsx +++ b/src/components/ActivityTypes/closeEndedQuestion.tsx @@ -1,7 +1,15 @@ import { CheckIcon, CloseIcon } from '@chakra-ui/icons'; import { Box, Button, Flex, Icon, Textarea, useToast } from '@chakra-ui/react'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import { PolyglotNodeValidation } from '../../types/polyglotElements'; +import { registerAnalyticsAction } from '../../data/AnalyticsFunctions'; +import { API } from '../../data/api'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + SubmitAction, + ZoneId, +} from '../../types/polyglotElements'; import FlexText from '../CostumTypography/FlexText'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; @@ -13,6 +21,10 @@ type CloseEndedToolProps = { setSatisfiedConditions: Dispatch>; showNextButton: boolean; setShowNextButton: Dispatch>; + userId: string; + flowId: string; + lastAction: string; + setLastAction: Dispatch>; }; type CloseEndedData = { @@ -29,6 +41,10 @@ const CloseEndedTool = ({ setSatisfiedConditions, showNextButton, setShowNextButton, + userId, + lastAction, + setLastAction, + flowId, }: CloseEndedToolProps) => { const [disable, setDisable] = useState(false); const [assessment, setAssessment] = useState(); @@ -36,14 +52,39 @@ const CloseEndedTool = ({ const [inputValue, setInputValue] = useState(''); useEffect(() => { + if (actualActivity?.type != 'closeEndedQuestionNode') return; if (!data) return; setDisable(false); setAssessment(''); //to move in validation button + + if (!isOpen) return; + try { + if (userId && actualActivity?._id) { + if (lastAction == 'open_node') return; + setLastAction('open_node'); + + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'open_node', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: flowId, + nodeId: actualActivity?._id, + activity: actualActivity.type, + }, + } as OpenCloseNodeAction); + } + } catch (e) { + console.log(e); + } }, [actualActivity]); + const toast = useToast(); if (!isOpen) return <>; - console.log('close ended activity'); + return ( edge !== 'undefined') ?? []; - if (edgesId) setSatisfiedConditions(edgesId); + if (edgesId) { + setSatisfiedConditions(edgesId); + const result = actualActivity?.validation.find((edge) => + edgesId.includes(edge.id) + )?.data.conditionKind as string; + console.log(result); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'submit_answer', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + //miss other values + flowId: flowId, + nodeId: actualActivity?._id, + exerciseType: actualActivity?.type, + answer: assessment, + result: result, + }, + } as SubmitAction); + } setShowNextButton(true); }} > diff --git a/src/components/ActivityTypes/multichoiceQuestion.tsx b/src/components/ActivityTypes/multichoiceQuestion.tsx index 5e6e669..fdac944 100644 --- a/src/components/ActivityTypes/multichoiceQuestion.tsx +++ b/src/components/ActivityTypes/multichoiceQuestion.tsx @@ -16,7 +16,15 @@ import { useEffect, useState, } from 'react'; -import { PolyglotNodeValidation } from '../../types/polyglotElements'; +import { registerAnalyticsAction } from '../../data/AnalyticsFunctions'; +import { API } from '../../data/api'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + SubmitAction, + ZoneId, +} from '../../types/polyglotElements'; import FlexText from '../CostumTypography/FlexText'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; @@ -27,6 +35,10 @@ type MultichoiceToolProps = { setSatisfiedConditions: Dispatch>; showNextButton: boolean; setShowNextButton: Dispatch>; + userId: string; + flowId: string; + lastAction: string; + setLastAction: Dispatch>; }; type MultichoiceQuestionData = { @@ -42,6 +54,10 @@ const MultichoiceTool = ({ setSatisfiedConditions, showNextButton, setShowNextButton, + userId, + lastAction, + setLastAction, + flowId, }: MultichoiceToolProps) => { const [disable, setDisable] = useState(false); const data = actualActivity?.data as MultichoiceQuestionData; @@ -52,10 +68,34 @@ const MultichoiceTool = ({ }, []); useEffect(() => { + if (actualActivity?.type != 'multipleChoiceQuestionNode') return; if (!data) return; setDisable(false); setCheckBoxValue(''); - //to move in validation button + + try { + if (!isOpen) return; + if (userId && actualActivity?._id) { + if (lastAction == 'open_node') return; + setLastAction('open_node'); + + console.log('choiceAction'); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'open_node', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: flowId, + nodeId: actualActivity._id, + activity: actualActivity.type, + }, + } as OpenCloseNodeAction); + } + } catch (e) { + console.log(e); + } }, [actualActivity]); const toast = useToast(); @@ -141,7 +181,27 @@ const MultichoiceTool = ({ }) .filter((edge) => edge !== 'undefined') ?? []; console.log(edgesId); - if (edgesId) setSatisfiedConditions(edgesId); + if (edgesId) { + setSatisfiedConditions(edgesId); + const result = actualActivity?.validation.find((edge) => + edgesId.includes(edge.id) + )?.data.conditionKind as string; + console.log(result); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'submit_answer', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: flowId, + nodeId: actualActivity?._id, + exerciseType: actualActivity?.type, + answer: checkBoxValue, + result: result, + }, + } as SubmitAction); + } setShowNextButton(true); }} > diff --git a/src/components/ActivityTypes/openQuestion.tsx b/src/components/ActivityTypes/openQuestion.tsx index af376c5..6d60457 100644 --- a/src/components/ActivityTypes/openQuestion.tsx +++ b/src/components/ActivityTypes/openQuestion.tsx @@ -12,7 +12,15 @@ import { import { AxiosResponse } from 'axios'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { API } from '../../data/api'; -import { PolyglotNodeValidation } from '../../types/polyglotElements'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + SubmitAction, + ZoneId, +} from '../../types/polyglotElements'; + +import { registerAnalyticsAction } from '../../data/AnalyticsFunctions'; import FlexText from '../CostumTypography/FlexText'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; @@ -24,6 +32,10 @@ type OpenQuestionToolProps = { setSatisfiedConditions: Dispatch>; showNextButton: boolean; setShowNextButton: Dispatch>; + userId: string; + flowId: string; + lastAction: string; + setLastAction: Dispatch>; }; type OpenQuestionData = { @@ -41,6 +53,10 @@ const OpenQuestionTool = ({ setSatisfiedConditions, showNextButton, setShowNextButton, + userId, + flowId, + lastAction, + setLastAction, }: OpenQuestionToolProps) => { const [isDisable, setDisable] = useState(false); const [isLoading, setLoading] = useState(false); @@ -49,11 +65,35 @@ const OpenQuestionTool = ({ const [inputValue, setInputValue] = useState(''); useEffect(() => { + if (!isOpen) return; if (!data) return; setDisable(false); setAssessment(''); //to move in validation button + + try { + if (userId && actualActivity?._id) { + if (lastAction == 'open_node') return; + setLastAction('open_node'); + + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'open_node', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: flowId, + nodeId: actualActivity._id, + activity: actualActivity.type, + }, + } as OpenCloseNodeAction); + } + } catch (e) { + console.log(e); + } }, [actualActivity]); + const toast = useToast(); if (!isOpen) return <>; console.log('open question activity'); @@ -152,7 +192,24 @@ const OpenQuestionTool = ({ .filter((edge) => edge !== 'undefined') ?? []; unlock(true); setDisable(true); - if (edgesId) setSatisfiedConditions(edgesId); + if (edgesId) { + setSatisfiedConditions(edgesId); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'submit_answer', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + //miss other values + flowId: flowId, + nodeId: actualActivity?._id, + exerciseType: actualActivity?.type, + answer: assessment, + result: data.isAnswerCorrect.toString(), + }, + } as SubmitAction); + } setLoading(false); setShowNextButton(true); } catch (err) { diff --git a/src/components/ActivityTypes/readMaterial.tsx b/src/components/ActivityTypes/readMaterial.tsx index 9c6ea2c..601075d 100644 --- a/src/components/ActivityTypes/readMaterial.tsx +++ b/src/components/ActivityTypes/readMaterial.tsx @@ -1,7 +1,14 @@ import { Box, Flex, Link } from '@chakra-ui/react'; +import { flow } from 'fp-ts/lib/function'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { registerAnalyticsAction } from '../../data/AnalyticsFunctions'; import { API } from '../../data/api'; -import { PolyglotNodeValidation } from '../../types/polyglotElements'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + ZoneId, +} from '../../types/polyglotElements'; import FlexText from '../CostumTypography/FlexText'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; @@ -12,6 +19,10 @@ type ReadMaterialToolProps = { unlock: Dispatch>; setSatisfiedConditions: Dispatch>; showNextButton: boolean; + userId: string; + flowId: string; + lastAction: string; + setLastAction: Dispatch>; }; type ReadMaterialData = { @@ -24,6 +35,10 @@ const ReadMaterialTool = ({ actualActivity, unlock, setSatisfiedConditions, + userId, + flowId, + lastAction, + setLastAction, }: ReadMaterialToolProps) => { const [pdfUrl, setPdfUrl] = useState(null); @@ -31,6 +46,7 @@ const ReadMaterialTool = ({ actualActivity?.data || ({ text: '', link: '' } as ReadMaterialData); useEffect(() => { + if (!isOpen) return; const fetchPdf = async () => { if (actualActivity?._id) { try { @@ -48,6 +64,30 @@ const ReadMaterialTool = ({ } }; fetchPdf(); + if (!data) return; + unlock(true); + const edgesId = actualActivity?.validation.map((edge) => edge.id); + if (edgesId != undefined) setSatisfiedConditions(edgesId); + try { + if (userId && actualActivity?._id) { + if (lastAction == 'open_node') return; + setLastAction('open_node'); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'open_node', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: flowId, + nodeId: actualActivity._id, + activity: actualActivity.type, + }, + } as OpenCloseNodeAction); + } + } catch (e) { + console.log(e); + } }, [actualActivity]); useEffect(() => { @@ -58,13 +98,6 @@ const ReadMaterialTool = ({ }; }, [pdfUrl]); - useEffect(() => { - if (!data) return; - unlock(true); - const edgesId = actualActivity?.validation.map((edge) => edge.id); - if (edgesId != undefined) setSatisfiedConditions(edgesId); - }, [actualActivity]); - if (!isOpen) return <>; return ( diff --git a/src/components/ActivityTypes/summary.tsx b/src/components/ActivityTypes/summary.tsx index 2f1ba51..c5e6cf4 100644 --- a/src/components/ActivityTypes/summary.tsx +++ b/src/components/ActivityTypes/summary.tsx @@ -1,7 +1,13 @@ import { Box, Button, Flex, Link, useClipboard } from '@chakra-ui/react'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { PolyglotNodeValidation } from '../../types/polyglotElements'; +import { registerAnalyticsAction } from '../../data/AnalyticsFunctions'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + ZoneId, +} from '../../types/polyglotElements'; import FlexText from '../CostumTypography/FlexText'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; @@ -13,6 +19,10 @@ type SummaryToolProps = { unlock: Dispatch>; setSatisfiedConditions: Dispatch>; showNextButton: boolean; + userId: string; + flowId: string; + lastAction: string; + setLastAction: Dispatch>; }; type SummaryData = { @@ -25,6 +35,10 @@ const SummaryTool = ({ actualActivity, unlock, setSatisfiedConditions, + userId, + lastAction, + setLastAction, + flowId, }: SummaryToolProps) => { const [summary, setSummary] = useState(''); const { onCopy } = useClipboard(summary || ''); @@ -33,10 +47,34 @@ const SummaryTool = ({ const data = actualActivity?.data || ({ text: '', link: '' } as SummaryData); useEffect(() => { + if (!isOpen) return; if (!data) return; unlock(true); const edgesId = actualActivity?.validation.map((edge) => edge.id); if (edgesId != undefined) setSatisfiedConditions(edgesId); + + try { + if (userId && actualActivity?._id) { + if (lastAction == 'open_node') return; + setLastAction('open_node'); + + console.log('summaryAction'); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'open_node', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: flowId, + nodeId: actualActivity._id, + activity: actualActivity.type, + }, + } as OpenCloseNodeAction); + } + } catch (e) { + console.log(e); + } }, [actualActivity]); if (!isOpen) return <>; diff --git a/src/components/ActivityTypes/trueFalse.tsx b/src/components/ActivityTypes/trueFalse.tsx index 6fcf51c..f9388ad 100644 --- a/src/components/ActivityTypes/trueFalse.tsx +++ b/src/components/ActivityTypes/trueFalse.tsx @@ -10,10 +10,19 @@ import { useToast, } from '@chakra-ui/react'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import { PolyglotNodeValidation } from '../../types/polyglotElements'; +import { registerAnalyticsAction } from '../../data/AnalyticsFunctions'; +import { API } from '../../data/api'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + SubmitAction, + ZoneId, +} from '../../types/polyglotElements'; import FlexText from '../CostumTypography/FlexText'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; + type TrueFalseToolProps = { isOpen: boolean; actualActivity: PolyglotNodeValidation | undefined; @@ -21,6 +30,10 @@ type TrueFalseToolProps = { setSatisfiedConditions: Dispatch>; showNextButton: boolean; setShowNextButton: Dispatch>; + userId: string; + flowId: string; + lastAction: string; + setLastAction: Dispatch>; }; type TrueFalseData = { @@ -38,12 +51,18 @@ const TrueFalseTool = ({ setSatisfiedConditions, showNextButton, setShowNextButton, + userId, + lastAction, + setLastAction, + flowId, }: TrueFalseToolProps) => { const [disable, setDisable] = useState(false); const data = actualActivity?.data as TrueFalseData; const [radioValue, setRadioValue] = useState<(string | null)[]>([]); useEffect(() => { + if (actualActivity?.type != 'TrueFalseNode') return; + console.log('open this shit'); if (!data) return; setDisable(false); const max = data.questions?.length; @@ -51,6 +70,28 @@ const TrueFalseTool = ({ for (let i = 0; i < max; i++) setup.push('true'); setRadioValue(setup); //to move in validation button + + try { + if (!isOpen) return; + if (userId && actualActivity?._id) { + if (lastAction == 'open_node') return; + setLastAction('open_node'); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'open_node', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: flowId, + nodeId: actualActivity._id, + activity: actualActivity.type, + }, + } as OpenCloseNodeAction); + } + } catch (e) { + console.log(e); + } }, [actualActivity]); const toast = useToast(); @@ -175,7 +216,7 @@ const TrueFalseTool = ({ ) return edge.id; else if ( - radioValue.length / 2 > total && + radioValue.length / 2 >= total && edge.data.conditionKind == 'fail' ) return edge.id; @@ -183,7 +224,28 @@ const TrueFalseTool = ({ }) .filter((edge) => edge !== 'undefined') ?? []; - if (edgesId) setSatisfiedConditions(edgesId); + if (edgesId) { + setSatisfiedConditions(edgesId); + /* + const result = actualActivity?.validation.find((edge)=> edgesId.includes(edge.id))?.data.conditionKind as string; + const answer = radioValue + .map((str, index) => `${str}: ${booleans[index]}`) + .join(", "); + console.log(result); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'submit_answer', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: flowId, + nodeId: actualActivity?._id, + exerciseType:actualActivity?.type, + answer: checkBoxValue, + result: result, + }} as SubmitAction)*/ + } setShowNextButton(true); }} > diff --git a/src/components/ActivityTypes/watchVideo.tsx b/src/components/ActivityTypes/watchVideo.tsx index 20c96d6..8d01fd6 100644 --- a/src/components/ActivityTypes/watchVideo.tsx +++ b/src/components/ActivityTypes/watchVideo.tsx @@ -1,7 +1,13 @@ import { ArrowRightIcon } from '@chakra-ui/icons'; import { Box, Link } from '@chakra-ui/react'; -import { Dispatch, SetStateAction, useEffect } from 'react'; -import { PolyglotNodeValidation } from '../../types/polyglotElements'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { registerAnalyticsAction } from '../../data/AnalyticsFunctions'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + ZoneId, +} from '../../types/polyglotElements'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; type WatchVideoToolProps = { @@ -9,6 +15,10 @@ type WatchVideoToolProps = { actualActivity: PolyglotNodeValidation | undefined; unlock: Dispatch>; setSatisfiedConditions: Dispatch>; + userId: string; + flowId: string; + lastAction: string; + setLastAction: Dispatch>; }; type WatchVideoData = { @@ -20,6 +30,10 @@ const WatchVideoTool = ({ actualActivity, unlock, setSatisfiedConditions, + userId, + lastAction, + setLastAction, + flowId, }: WatchVideoToolProps) => { if (!isOpen) return <>; console.log('data check ' + actualActivity); @@ -34,10 +48,35 @@ const WatchVideoTool = ({ : null; // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { + if (actualActivity?.type != 'WatchVideoNode') return; if (!data) return; unlock(true); const edgesId = actualActivity?.validation.map((edge) => edge.id); if (edgesId != undefined) setSatisfiedConditions(edgesId); + + try { + if (!isOpen) return; + if (userId && actualActivity?._id) { + if (lastAction == 'open_node') return; + setLastAction('open_node'); + + console.log('watcgAction'); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'open_node', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: flowId, + nodeId: actualActivity._id, + activity: actualActivity.type, + }, + } as OpenCloseNodeAction); + } + } catch (e) { + console.log(e); + } }, [actualActivity]); return ( diff --git a/src/components/GamifiedUI/NumberDisplay.tsx b/src/components/GamifiedUI/NumberDisplay.tsx new file mode 100644 index 0000000..749379d --- /dev/null +++ b/src/components/GamifiedUI/NumberDisplay.tsx @@ -0,0 +1,87 @@ +import { Box, Button, Grid, VStack } from '@chakra-ui/react'; +import React, { useState } from 'react'; + +type NumberedDisplayType = { + isOpen: boolean; + onEnterAction: (code: string) => any; + text: string; +}; + +const NumberedDisplay = ({ + isOpen, + onEnterAction, + text, +}: NumberedDisplayType) => { + const [code, setCode] = useState(''); + + const handleInput = (value: string) => { + if (value === 'delete') { + setCode(code.slice(0, -1)); + } else if (value === 'enter') { + onEnterAction(code); + } else { + setCode(code + value); + } + }; + + if (!isOpen) return <>; + return ( + + + {code || text} + + + {[...Array(9).keys()].map((num) => ( + + ))} + + + + + + ); +}; + +export default NumberedDisplay; diff --git a/src/components/Modals/AIToolModal.tsx b/src/components/Modals/AIToolModal.tsx index 4549253..bac8484 100644 --- a/src/components/Modals/AIToolModal.tsx +++ b/src/components/Modals/AIToolModal.tsx @@ -24,7 +24,7 @@ import { AxiosResponse } from 'axios'; import { useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { API } from '../../data/api'; -import { TypeOfExercise } from '../../types/polyglotElements/AIGenerativeTypes/AIGenerativeTypes'; +import { TypeOfExercise } from '../../types/polyglotElements'; export type ModelTemplateProps = { isOpen: boolean; diff --git a/src/data/AnalyticsFunctions.ts b/src/data/AnalyticsFunctions.ts new file mode 100644 index 0000000..95ee0e0 --- /dev/null +++ b/src/data/AnalyticsFunctions.ts @@ -0,0 +1,62 @@ +//functions to interact with learning analytics main function defined as generics for scalability to different usage +import { AnalyticsActionBody } from '../types/polyglotElements'; +import { API } from './api'; + +export async function registerAnalyticsAction( + actionRegistred: T +): Promise { + if (actionRegistred.userId == '') return; + if ('actionType' in actionRegistred) { + switch (actionRegistred.actionType) { + case 'remove_LP_selection': + case 'open_LP_info': + case 'close_LP_info': + case 'select_LP': + if (!('flowId' in actionRegistred.action)) { + console.log('Invalid structure, missing flowId'); + return; + } + break; + case 'open_node': + case 'close_node': + console.log(actionRegistred.action.flowId); + if ( + !( + 'flowId' in actionRegistred.action && + 'nodeId' in actionRegistred.action && + 'activity' in actionRegistred.action + ) + ) { + console.log('Invalid OpenCloseToolAction structure'); + return; + } + break; + case 'submit_answer': + if ( + !( + 'flowId' in actionRegistred.action && + 'nodeId' in actionRegistred.action && + 'exerciseType' in actionRegistred.action && + 'answer' in actionRegistred.action && + 'result' in actionRegistred.action + ) + ) { + console.log( + 'Invalid structure for SubmitAction, missing required fields' + ); + return; + } + break; + default: + if (actionRegistred.action != undefined) { + console.log(`Unknown actionType: ${actionRegistred.actionType}`); + return; + } + } + } + try { + await API.registerAction(actionRegistred); + } catch (error) { + console.log(error); + } +} diff --git a/src/data/api.ts b/src/data/api.ts index 01acaf0..0bfe27b 100644 --- a/src/data/api.ts +++ b/src/data/api.ts @@ -1,17 +1,16 @@ import axiosCreate, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; -import { - ManualProgressInfo, - PolyglotFlow, - ProgressInfo, -} from '../types/polyglotElements'; import { AIExerciseType, AnalyseType, + AnalyticsActionBody, CorrectorType, LOType, + ManualProgressInfo, MaterialType, + PolyglotFlow, + ProgressInfo, SummarizeType, -} from '../types/polyglotElements/AIGenerativeTypes/AIGenerativeTypes'; +} from '../types/polyglotElements'; export type aiAPIResponse = { Date: string; @@ -20,14 +19,6 @@ export type aiAPIResponse = { }; const axios = axiosCreate.create({ - baseURL: 'https://polyglot-api-staging.polyglot-edu.com/', - headers: { - 'Content-Type': 'application/json', - }, - withCredentials: false, -}); - -const axiosProgress = axiosCreate.create({ baseURL: 'https://polyglot-api-staging.polyglot-edu.com', headers: { 'Content-Type': 'application/json', @@ -47,41 +38,35 @@ export const API = { return axios.get(`/api/flows` + queryParams); }, progressInfo: (body: ProgressInfo): Promise => { - return axiosProgress.post<{}, AxiosResponse, {}>( + return axios.post<{}, AxiosResponse, {}>( `/api/execution/progressInfo`, body ); }, manualProgress: (body: ManualProgressInfo): Promise => { - return axiosProgress.post<{}, AxiosResponse, {}>( + return axios.post<{}, AxiosResponse, {}>( `/api/execution/progressAction`, body ); }, resetProgress: (body: ManualProgressInfo): Promise => { - return axiosProgress.post<{}, AxiosResponse, {}>( + return axios.post<{}, AxiosResponse, {}>( `/api/execution/resetProgress`, body ); }, getActualNodeInfo: (body: { ctxId: string }): Promise => { - return axiosProgress.post<{}, AxiosResponse, {}>( - `/api/execution/actual`, - body - ); + return axios.post<{}, AxiosResponse, {}>(`/api/execution/actual`, body); }, nextNodeProgression: (body: { ctxId: string; satisfiedConditions: string[]; }): Promise => { - return axiosProgress.post<{}, AxiosResponse, {}>( - `/api/execution/next`, - body - ); + return axios.post<{}, AxiosResponse, {}>(`/api/execution/next`, body); }, analyseMaterial: (body: AnalyseType): Promise => { @@ -117,18 +102,25 @@ export const API = { }, corrector: (body: CorrectorType): Promise => { - return axiosProgress.post<{}, AxiosResponse, {}>( - '/api/openai/Corrector', - body - ); + return axios.post<{}, AxiosResponse, {}>('/api/openai/Corrector', body); }, downloadFile: (body: { nodeId: string }): Promise => { - return axiosProgress.get<{}, AxiosResponse, {}>( + return axios.get<{}, AxiosResponse, {}>( `/api/file/download/${body.nodeId}`, { responseType: 'blob', } ); }, + //learning Analysis API + getAllActions: (): Promise => { + return axios.get<{}, AxiosResponse, {}>(`/api/learningAnalytics/`); + }, + //register action + registerAction: ( + body: AnalyticsActionBody /*NextBody*/ + ): Promise => { + return axios.post<{}, AxiosResponse, {}>(`/api/learningAnalytics/`, body); + }, }; diff --git a/src/pages/flowMenu/index.tsx b/src/pages/flowMenu/index.tsx index 646093a..7f2bb64 100644 --- a/src/pages/flowMenu/index.tsx +++ b/src/pages/flowMenu/index.tsx @@ -42,12 +42,16 @@ import { flowLearningExecutionOrder } from '../../algorithms/flowAlgo'; import HeadingSubtitle from '../../components/CostumTypography/HeadingSubtitle'; import HeadingTitle from '../../components/CostumTypography/HeadingTitle'; import Navbar from '../../components/NavBars/NavBar'; +import { registerAnalyticsAction } from '../../data/AnalyticsFunctions'; import { API } from '../../data/api'; import defaultIcon from '../../public/summary_CasesEvaluation_icon.png'; import { nodeIconsMapping, + Platform, PolyglotFlow, PolyglotNode, + SelectRemoveLPAction, + ZoneId, } from '../../types/polyglotElements'; const activeFlowList = [ //'d775f1fa-a014-4d2a-9677-a1aa7c45f2af', //UML chronicles mission1 @@ -75,6 +79,8 @@ const FlowListIndex = () => { const [searchTerm, setSearchTerm] = useState(''); const [allTags, setTags] = useState<{ name: string; color: string }[]>(); const [selectedTags, setSelectedTags] = useState([]); + const [scriptCheck, setScriptCheck] = useState(false); + const [userId, setUserId] = useState(''); useEffect(() => { const script = document.createElement('script'); @@ -82,21 +88,35 @@ const FlowListIndex = () => { script.src = 'https://play.workadventu.re/iframe_api.js'; script.async = true; + script.onload = () => { + setScriptCheck(true); + }; + document.body.appendChild(script); + return () => { + document.body.removeChild(script); + }; + }, []); + + useEffect(() => { + if (!scriptCheck) return; API.loadFlowList() .then((response) => { setFlows(response.data); }) .catch((error) => { - console.error('There was a problem with the fetch operation:', error); + console.error(error); }); - return () => { - document.body.removeChild(script); - }; - }, []); + }, [scriptCheck]); + useEffect(() => { try { + try { + setUserId(WA.player.uuid || 'guest'); + } catch (error: any) { + setUserId('guest'); + } const WAStateFlow = WA.player.state.actualFlow; if ( WAStateFlow && @@ -109,8 +129,8 @@ const FlowListIndex = () => { (flow) => flow._id == (WA.player.state.actualFlow as string) )[0] ); - } catch (error: any) { - console.log(error); + } catch (error) { + console.error(error); } }, [flows]); @@ -226,6 +246,19 @@ const FlowListIndex = () => { height="30px" width="20px" onClick={() => { + try { + const action: SelectRemoveLPAction = { + timestamp: new Date(), + userId: userId, + actionType: 'remove_LP_selection', + platform: Platform.WorkAdventure, + zoneId: ZoneId.LearningPathSelectionZone, + action: { flowId: selectedFlow?._id || '' }, + }; + registerAnalyticsAction(action); + } catch (e) { + console.log(e); + } WA.player.state.actualFlow = null; setSelectedFlow(null); }} @@ -559,6 +592,33 @@ const FlowListIndex = () => { : 'Click to select this flow' } onClick={() => { + try { + const action: SelectRemoveLPAction = + selectedFlow == currentFlow + ? { + timestamp: new Date(), + userId: userId, + actionType: 'remove_LP_selection', + platform: Platform.WorkAdventure, + zoneId: ZoneId.LearningPathSelectionZone, + action: { flowId: selectedFlow?._id || '' }, + } + : { + timestamp: new Date(), + userId: userId, + actionType: 'select_LP', + platform: Platform.WorkAdventure, + zoneId: ZoneId.LearningPathSelectionZone, + action: { + flowId: + currentFlow?._id || selectedFlow?._id || '', + }, + }; + + registerAnalyticsAction(action); + } catch (e) { + console.log(e); + } WA.player.state.actualFlow == currentFlow?._id ? (WA.player.state.actualFlow = null) : (WA.player.state.actualFlow = currentFlow?._id); @@ -574,7 +634,7 @@ const FlowListIndex = () => { _hover: { bg: 'gray.300' }, }} > - {selectedFlow == currentFlow ? 'Selected' : 'Select LP'} + {selectedFlow == currentFlow ? 'Remove LP' : 'Select LP'} diff --git a/src/pages/flowShower/[id]/index.tsx b/src/pages/flowShower/[id]/index.tsx index 6d3b623..3786234 100644 --- a/src/pages/flowShower/[id]/index.tsx +++ b/src/pages/flowShower/[id]/index.tsx @@ -19,8 +19,14 @@ import { UnorderedList, } from '@chakra-ui/react'; import { useRouter } from 'next/router'; +import { registerAnalyticsAction } from '../../../data/AnalyticsFunctions'; import { API } from '../../../data/api'; -import { PolyglotFlow } from '../../../types/polyglotElements'; +import { + OpenLPInfoAction, + Platform, + PolyglotFlow, + ZoneId, +} from '../../../types/polyglotElements'; enum list { 'multipleChoiceQuestionNode' = 'Multichoice Question', @@ -38,7 +44,7 @@ function FlowShower() { const router = useRouter(); const { flowId } = useMemo( () => ({ - flowId: router.query?.id?.toString(), + flowId: router.query?.id?.toString() || 'info', }), [router.query?.id] ); @@ -51,16 +57,70 @@ function FlowShower() { type: 'Not defined', }, ]); - - console.log(flowId); const [flow, setFlow] = useState(); + const [scriptCheck, setScriptCheck] = useState(false); + const [userId, setUserId] = useState('guest'); + + useEffect(() => { + const script = document.createElement('script'); + + script.src = 'https://play.workadventu.re/iframe_api.js'; + script.async = true; + + script.onload = () => { + setScriptCheck(true); + }; + + document.body.appendChild(script); + + return () => { + document.body.removeChild(script); + }; + }, []); + + useEffect(() => { + if (!scriptCheck) return; + try { + setUserId(WA.player.uuid || 'guest'); + } catch (e) { + setUserId('guest'); + } + const action: OpenLPInfoAction = { + timestamp: new Date(), + userId: userId, + actionType: 'open_LP_info', + platform: Platform.WorkAdventure, + zoneId: ZoneId.InstructionWebpageZone, + action: { flowId: flowId }, + }; + + registerAnalyticsAction(action); + + const handleBeforeUnload = () => { + const action: OpenLPInfoAction = { + timestamp: new Date(), + userId: userId, + actionType: 'close_LP_info', + platform: Platform.WorkAdventure, + zoneId: ZoneId.InstructionWebpageZone, + action: { flowId: flowId }, + }; + + registerAnalyticsAction(action); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [scriptCheck]); + useEffect(() => { - console.log(flowId); - if (flowId) + if (flowId != 'null' && flowId != 'info') API.loadFlowElementsAsync(flowId) .then((response) => { setFlow(response.data); - console.log(flow); }) .catch((error) => { console.error('There was a problem with the fetch operation:', error); diff --git a/src/pages/flowShower/index.tsx b/src/pages/flowShower/index.tsx index b0f07e8..95c6915 100644 --- a/src/pages/flowShower/index.tsx +++ b/src/pages/flowShower/index.tsx @@ -10,8 +10,72 @@ import { Text, UnorderedList, } from '@chakra-ui/react'; +import { registerAnalyticsAction } from '../../data/AnalyticsFunctions'; +import { + OpenLPInfoAction, + Platform, + ZoneId, +} from '../../types/polyglotElements'; function FlowShower() { + const [scriptCheck, setScriptCheck] = useState(false); + const [userId, setUserId] = useState('guest'); + + useEffect(() => { + const script = document.createElement('script'); + + script.src = 'https://play.workadventu.re/iframe_api.js'; + script.async = true; + + script.onload = () => { + setScriptCheck(true); + }; + + document.body.appendChild(script); + + return () => { + document.body.removeChild(script); + }; + }, []); + + useEffect(() => { + if (!scriptCheck) return; + try { + setUserId(WA.player.uuid || 'guest'); + } catch (e) { + setUserId('guest'); + } + const action: OpenLPInfoAction = { + timestamp: new Date(), + userId: userId, + actionType: 'open_LP_info', + platform: Platform.WorkAdventure, + zoneId: ZoneId.InstructionWebpageZone, + action: { flowId: 'none' }, + }; + + registerAnalyticsAction(action); + + const handleBeforeUnload = () => { + const action: OpenLPInfoAction = { + timestamp: new Date(), + userId: userId, + actionType: 'close_LP_info', + platform: Platform.WorkAdventure, + zoneId: ZoneId.InstructionWebpageZone, + action: { flowId: 'none' }, + }; + + registerAnalyticsAction(action); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [scriptCheck]); + return (
diff --git a/src/pages/gamifiedUI/index.tsx b/src/pages/gamifiedUI/index.tsx new file mode 100644 index 0000000..7ecbc0b --- /dev/null +++ b/src/pages/gamifiedUI/index.tsx @@ -0,0 +1,154 @@ +/* eslint-disable react/no-unescaped-entities */ +/* eslint-disable no-restricted-globals */ +import { CloseIcon } from '@chakra-ui/icons'; +import { Box, IconButton, useToast } from '@chakra-ui/react'; +import React, { useEffect, useState } from 'react'; +import NumberedDisplay from '../../components/GamifiedUI/NumberDisplay'; + +/* +import { AnalyticsActionBody, GradeAction, Platform, ZoneId } from '../../types/polyglotElements'; +import { API } from '../../data/api'; + +//setup for creation action by UI + +function registerAnalyticsAction( + action: T +): void { + if ('actionType' in action) { + switch (action.actionType) { + case 'gradeAction': + if (!('flow' in action.action && 'grade' in action.action)) { + throw new Error('Invalid GradeAction structure'); + } + break; + case 'completeLPAction': + break; + default: + throw new Error(`Unknown actionType: ${action.actionType}`); + } + } + API.registerAction(action); +} + +const action: GradeAction = { + timestamp: new Date(), + userId: WA.player.name, + actionType: 'GradeAction', + zoneId: ZoneId.FreeZone, + platform: Platform.WorkAdventure, + action: { + flow: 'test', + grade: 5, + }, +}; +registerAnalyticsAction(action); +*/ + +function GamifiedUI() { + const [studyRoomCode, setStudyRoomCode] = useState(''); + const [sectorName, setSectorName] = useState(''); + const [scriptCheck, setScriptCheck] = useState(false); + const toast = useToast(); + + const checkCodeStudyRoom = (code: string) => { + setStudyRoomCode(code); + if (!scriptCheck) return; + WA.player.state.studyRoomCode = code; + }; + + const setCodeStudyRoom = (code: string) => { + setStudyRoomCode('True'); + WA.state.saveVariable(studyRoomCode, code); + toast({ + title: 'Validation error', + description: 'Password correctly registred.', + status: 'success', + duration: 3000, + position: 'top-left', + isClosable: true, + }); + }; + useEffect(() => { + const script = document.createElement('script'); + + script.src = 'https://play.workadventu.re/iframe_api.js'; + script.async = true; + script.onload = () => { + setScriptCheck(true); + }; + + document.body.appendChild(script); + return () => { + document.body.removeChild(script); + }; + }, []); + + useEffect(() => { + if (!scriptCheck) return; + setStudyRoomCode((WA.player.state.studyRoomCode as string) || ''); + setSectorName((WA.player.state.sectorName as string) || ''); + const studyRoomSub = WA.player.state + .onVariableChange('studyRoomCode') + .subscribe((value) => { + if (value == studyRoomCode) return; + setStudyRoomCode((value as string) || ''); + if (studyRoomCode == 'Error') + toast({ + title: 'Code error', + description: + 'There is no room with a matching code, please try again or enter a room to create your own session.', + status: 'error', + duration: 3000, + position: 'top-left', + isClosable: true, + }); + if (studyRoomCode == 'True') + toast({ + title: 'Validation', + description: 'Password correctly registred.', + status: 'success', + duration: 3000, + position: 'top-left', + isClosable: true, + }); + }); + const sectorNameSub = WA.player.state + .onVariableChange('sectorName') + .subscribe((value) => { + if (value == sectorName) return; + setStudyRoomCode((value as string) || ''); + console.log('sectorName change on webApp'); + setSectorName((value as string) || ''); + }); + return () => { + studyRoomSub.unsubscribe(); + sectorNameSub.unsubscribe(); + }; + }, [scriptCheck]); + + return ( + + + + + ); +} + +export default GamifiedUI; diff --git a/src/pages/tools/[id]/index.tsx b/src/pages/tools/[id]/index.tsx index f4e921a..524f88c 100644 --- a/src/pages/tools/[id]/index.tsx +++ b/src/pages/tools/[id]/index.tsx @@ -21,23 +21,35 @@ import SummaryTool from '../../../components/ActivityTypes/summary'; import TrueFalseTool from '../../../components/ActivityTypes/trueFalse'; import WatchVideoTool from '../../../components/ActivityTypes/watchVideo'; import Navbar from '../../../components/NavBars/NavBar'; +import { registerAnalyticsAction } from '../../../data/AnalyticsFunctions'; import { API } from '../../../data/api'; -import { PolyglotNodeValidation } from '../../../types/polyglotElements'; +import { + OpenCloseNodeAction, + OpenCloseTool, + Platform, + PolyglotNodeValidation, + ZoneId, +} from '../../../types/polyglotElements'; import auth0 from '../../../utils/auth0'; const FlowIndex = () => { // Calling bootstrapExtra will initiliaze all the "custom properties" //bootstrapExtra(); const [actualData, setActualData] = useState(); + const [flowId, setFlowId] = useState(''); const [unlock, setUnlock] = useState(false); const [satisfiedConditions, setSatisfiedConditions] = useState([]); const router = useRouter(); const ctx = router.query?.id?.toString(); const [showNextButton, setShowNextButton] = useState(false); + const [scriptCheck, setScriptCheck] = useState(false); + const [userId, setUserId] = useState(''); + const [lastAction, setAction] = useState(''); useEffect(() => { if (ctx != undefined) API.getActualNodeInfo({ ctxId: ctx }).then((resp) => { + setFlowId(resp.data.flowId); setActualData(resp.data); setUnlock(false); }); @@ -46,13 +58,66 @@ const FlowIndex = () => { script.src = 'https://play.workadventu.re/iframe_api.js'; script.async = true; + script.onload = () => { + setScriptCheck(true); + }; + document.body.appendChild(script); + if (ctx != undefined) + API.getActualNodeInfo({ ctxId: ctx }).then((resp) => { + setActualData(resp.data); + setUnlock(false); + }); return () => { document.body.removeChild(script); }; }, []); + useEffect(() => { + if (!scriptCheck) return; + console.log('script checked'); + try { + setUserId(WA.player.uuid || 'guest'); + } catch (e) { + setUserId('guest'); + } + }, [scriptCheck]); + + useEffect(() => { + if (userId != '') { + //problemi nella creazione delle azioni controlla -> flowShower e FlowMenu sono ok da controllare solo lo stato di WA.player + + setAction('open_tool'); + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'open_tool', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: undefined, + } as OpenCloseTool); + setScriptCheck(false); //debug to run only one time + + const handleBeforeUnload = () => { + registerAnalyticsAction({ + timestamp: new Date(), + userId: userId, + actionType: 'close_tool', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: undefined, + } as OpenCloseTool); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + } + }, [userId]); + return ( {/* if is loading */} @@ -89,12 +154,20 @@ const FlowIndex = () => { unlock={setUnlock} setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} + userId={userId} + flowId={flowId} + lastAction={lastAction} + setLastAction={setAction} /> { setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} setShowNextButton={setShowNextButton} + userId={userId} + flowId={flowId} + lastAction={lastAction} + setLastAction={setAction} /> { setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} setShowNextButton={setShowNextButton} + userId={userId} + flowId={flowId} + lastAction={lastAction} + setLastAction={setAction} /> { setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} setShowNextButton={setShowNextButton} + userId={userId} + flowId={flowId} + lastAction={lastAction} + setLastAction={setAction} /> { setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} setShowNextButton={setShowNextButton} + userId={userId} + flowId={flowId} + lastAction={lastAction} + setLastAction={setAction} /> { unlock={setUnlock} setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} + userId={userId} + flowId={flowId} + lastAction={lastAction} + setLastAction={setAction} />