From 2d9c9f4df48d10f3d6ac00d7c44943dd14f3c3f4 Mon Sep 17 00:00:00 2001 From: tmaog Date: Tue, 1 Apr 2025 10:56:56 +0200 Subject: [PATCH 01/19] setup for analytics actions interaction --- src/components/Modals/AIToolModal.tsx | 2 +- src/data/AnalyticsFunctions.ts | 36 +++++++++++ src/data/api.ts | 52 +++++++--------- src/pages/flowMenu/index.tsx | 30 +++++++++- src/pages/flowShower/[id]/index.tsx | 50 ++++++++++++++-- src/pages/tools/[id]/index.tsx | 59 +++++++++++++++++-- .../AIGenerativeTypes/index.ts | 1 + .../analytics/AnalyticsTypes.ts | 59 +++++++++++++++++++ src/types/polyglotElements/analytics/index.ts | 1 + src/types/polyglotElements/index.ts | 2 + 10 files changed, 250 insertions(+), 42 deletions(-) create mode 100644 src/data/AnalyticsFunctions.ts create mode 100644 src/types/polyglotElements/AIGenerativeTypes/index.ts create mode 100644 src/types/polyglotElements/analytics/AnalyticsTypes.ts create mode 100644 src/types/polyglotElements/analytics/index.ts 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..3a81bf9 --- /dev/null +++ b/src/data/AnalyticsFunctions.ts @@ -0,0 +1,36 @@ +//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 function registerAnalyticsAction( + actionRegistred: T +): void { + if ('actionType' in actionRegistred) { + switch (actionRegistred.actionType) { + case 'removeLPSelectionAction': + case 'openLPInfoAction': + case 'selectLPAction': + case 'openLPInfoAction': + if (!('flowId' in actionRegistred.action)) { + throw new Error('Invalid structure, missing flowId'); + } + break; + case 'closeToolAction': + case 'openToolAction': + if ( + !( + 'flowId' in actionRegistred.action && + 'nodeId' in actionRegistred.action && + 'activity' in actionRegistred.action + ) + ) { + throw new Error('Invalid OpenCloseToolAction structure'); + } + break; + default: + throw new Error(`Unknown actionType: ${actionRegistred.actionType}`); + } + } + + API.registerAction(actionRegistred); +} diff --git a/src/data/api.ts b/src/data/api.ts index 01acaf0..28f1244 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/`); + }, + 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..778351b 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 @@ -95,6 +99,7 @@ const FlowListIndex = () => { document.body.removeChild(script); }; }, []); + useEffect(() => { try { const WAStateFlow = WA.player.state.actualFlow; @@ -559,6 +564,29 @@ const FlowListIndex = () => { : 'Click to select this flow' } onClick={() => { + const action: SelectRemoveLPAction = + selectedFlow == currentFlow + ? { + timestamp: new Date(), + userId: WA.player.name || '', + actionType: 'removeLPSelectionAction', + platform: Platform.WorkAdventure, + zoneId: ZoneId.FreeZone, + action: { flowId: selectedFlow?._id || '' }, + } + : { + timestamp: new Date(), + userId: WA.player.name || '', + actionType: 'selectLPAction', + platform: Platform.WorkAdventure, + zoneId: ZoneId.FreeZone, + action: { + flowId: currentFlow?._id || selectedFlow?._id || '', + }, + }; + + registerAnalyticsAction(action); + WA.player.state.actualFlow == currentFlow?._id ? (WA.player.state.actualFlow = null) : (WA.player.state.actualFlow = currentFlow?._id); @@ -574,7 +602,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..392df41 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', @@ -52,19 +58,55 @@ function FlowShower() { }, ]); - console.log(flowId); const [flow, setFlow] = useState(); + + useEffect(() => { + const script = document.createElement('script'); + + script.src = 'https://play.workadventu.re/iframe_api.js'; + script.async = true; + + document.body.appendChild(script); + }, []); + useEffect(() => { - console.log(flowId); if (flowId) API.loadFlowElementsAsync(flowId) .then((response) => { setFlow(response.data); - console.log(flow); }) .catch((error) => { console.error('There was a problem with the fetch operation:', error); }); + const action: OpenLPInfoAction = { + timestamp: new Date(), + userId: WA.player.name || '', + actionType: 'openLPInfoAction', + platform: Platform.WorkAdventure, + zoneId: ZoneId.FreeZone, + action: { flowId: flowId || '' }, + }; + + registerAnalyticsAction(action); + + const handleBeforeUnload = () => { + const action: OpenLPInfoAction = { + timestamp: new Date(), + userId: WA.player.name || '', + actionType: 'closeLPInfoAction', + platform: Platform.WorkAdventure, + zoneId: ZoneId.FreeZone, + action: { flowId: flowId || '' }, + }; + + registerAnalyticsAction(action); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; }, [flowId]); useEffect(() => { diff --git a/src/pages/tools/[id]/index.tsx b/src/pages/tools/[id]/index.tsx index f4e921a..32ea1cc 100644 --- a/src/pages/tools/[id]/index.tsx +++ b/src/pages/tools/[id]/index.tsx @@ -21,8 +21,14 @@ 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 { + OpenCloseTool, + Platform, + PolyglotNodeValidation, + ZoneId, +} from '../../../types/polyglotElements'; import auth0 from '../../../utils/auth0'; const FlowIndex = () => { @@ -36,23 +42,64 @@ const FlowIndex = () => { const [showNextButton, setShowNextButton] = useState(false); useEffect(() => { - if (ctx != undefined) - API.getActualNodeInfo({ ctxId: ctx }).then((resp) => { - setActualData(resp.data); - setUnlock(false); - }); const script = document.createElement('script'); script.src = 'https://play.workadventu.re/iframe_api.js'; script.async = 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 (actualData) { + const action: OpenCloseTool = { + timestamp: new Date(), + userId: WA.player.name || '', + actionType: 'openToolAction', + platform: Platform.WebApp, + zoneId: ZoneId.WebAppZone, + action: { + flowId: actualData._id, //al momento non c'รจ flowId su PolyglotNodeValidation + nodeId: actualData._id, + activity: actualData.type, + }, + }; + + registerAnalyticsAction(action); + } + const handleBeforeUnload = () => { + const action: OpenCloseTool = { + timestamp: new Date(), + userId: WA.player.name || '', + actionType: 'closeToolAction', + platform: Platform.WorkAdventure, + zoneId: ZoneId.FreeZone, + action: { + flowId: actualData?._id || '', //al momento non c'รจ flowId su PolyglotNodeValidation + nodeId: actualData?._id || '', + activity: actualData?.type || 'wrongType', + }, + }; + + registerAnalyticsAction(action); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [actualData]); + return ( {/* if is loading */} diff --git a/src/types/polyglotElements/AIGenerativeTypes/index.ts b/src/types/polyglotElements/AIGenerativeTypes/index.ts new file mode 100644 index 0000000..104b41d --- /dev/null +++ b/src/types/polyglotElements/AIGenerativeTypes/index.ts @@ -0,0 +1 @@ +export * from './AIGenerativeTypes'; diff --git a/src/types/polyglotElements/analytics/AnalyticsTypes.ts b/src/types/polyglotElements/analytics/AnalyticsTypes.ts new file mode 100644 index 0000000..19d31ad --- /dev/null +++ b/src/types/polyglotElements/analytics/AnalyticsTypes.ts @@ -0,0 +1,59 @@ +import { StringPartToDelimiterCase } from 'type-fest/source/delimiter-case'; + +export enum Platform { + PolyGloT, + VirtualStudio, + Papyrus, + WebApp, + WorkAdventure, +} + +export enum ZoneId { + FreeZone, + OutsideZone, + SilentZone, + LearningPathSelectionZone, + InstructionWebpageZone, + WebAppZone, + MeetingRoomZone, + PolyGlotLearningZone, + PolyGlotLearningPathCreationZone, + PapyrusWebZone, + VirtualStudioZone, +} + +//Action Body +export type AnalyticsActionBody = { + timestamp: Date; + userId: string; + actionType: string; + zoneId: ZoneId; + platform: Platform; + action: any; +}; + +export type OpenLPInfoAction = AnalyticsActionBody & { + action: { + flowId: string; + }; +}; + +export type CloseLPInfoAction = AnalyticsActionBody & { + action: { + flowId: string; + }; +}; + +export type SelectRemoveLPAction = AnalyticsActionBody & { + action: { + flowId: string; + }; +}; + +export type OpenCloseTool = AnalyticsActionBody & { + action: { + flowId: string; + nodeId: string; + activity: string; + }; +}; diff --git a/src/types/polyglotElements/analytics/index.ts b/src/types/polyglotElements/analytics/index.ts new file mode 100644 index 0000000..0c9e3e0 --- /dev/null +++ b/src/types/polyglotElements/analytics/index.ts @@ -0,0 +1 @@ +export * from './AnalyticsTypes'; diff --git a/src/types/polyglotElements/index.ts b/src/types/polyglotElements/index.ts index 5d68043..6e560cc 100644 --- a/src/types/polyglotElements/index.ts +++ b/src/types/polyglotElements/index.ts @@ -1,3 +1,5 @@ +export * from './AIGenerativeTypes'; +export * from './analytics'; export * from './edges'; export * from './flow'; export * from './nodes'; From 26fd610c2536ac666012d8f1c4f5987c1346de9c Mon Sep 17 00:00:00 2001 From: tmaog Date: Tue, 8 Apr 2025 16:17:50 +0200 Subject: [PATCH 02/19] feat: numberDisplay UI --- src/components/GamifiedUI/NumberDisplay.tsx | 87 +++++++++++++++++++++ src/pages/flowShower/index.tsx | 2 +- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/components/GamifiedUI/NumberDisplay.tsx 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/pages/flowShower/index.tsx b/src/pages/flowShower/index.tsx index b0f07e8..2b79f00 100644 --- a/src/pages/flowShower/index.tsx +++ b/src/pages/flowShower/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unescaped-entities */ /* eslint-disable no-restricted-globals */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; //import "../FlowShower.css"; // Ensure this CSS file is updated for new styles import { Box, From 346607984734db8bd36a2efa63f70636d63f246a Mon Sep 17 00:00:00 2001 From: tmaog Date: Tue, 8 Apr 2025 16:58:35 +0200 Subject: [PATCH 03/19] feat: creation of open-close tool action and open - close node action --- .../ActivityTypes/closeEndedQuestion.tsx | 38 +++++- .../ActivityTypes/multichoiceQuestion.tsx | 39 +++++- src/components/ActivityTypes/openQuestion.tsx | 38 +++++- src/components/ActivityTypes/readMaterial.tsx | 38 +++++- src/components/ActivityTypes/summary.tsx | 38 +++++- src/components/ActivityTypes/trueFalse.tsx | 39 +++++- src/components/ActivityTypes/watchVideo.tsx | 38 +++++- src/data/api.ts | 7 ++ src/pages/gamifiedUI/index.tsx | 114 ++++++++++++++++++ src/pages/tools/[id]/index.tsx | 43 ++++++- .../ActionTypes/ActionTypes.ts | 44 +++++++ .../polyglotElements/ActionTypes/index.ts | 1 + src/types/polyglotElements/index.ts | 1 + .../polyglotElements/nodes/PolyglotNode.ts | 1 + 14 files changed, 470 insertions(+), 9 deletions(-) create mode 100644 src/pages/gamifiedUI/index.tsx create mode 100644 src/types/polyglotElements/ActionTypes/ActionTypes.ts create mode 100644 src/types/polyglotElements/ActionTypes/index.ts diff --git a/src/components/ActivityTypes/closeEndedQuestion.tsx b/src/components/ActivityTypes/closeEndedQuestion.tsx index 8d43eff..2286991 100644 --- a/src/components/ActivityTypes/closeEndedQuestion.tsx +++ b/src/components/ActivityTypes/closeEndedQuestion.tsx @@ -1,7 +1,13 @@ 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 { API } from '../../data/api'; +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,7 @@ type CloseEndedToolProps = { setSatisfiedConditions: Dispatch>; showNextButton: boolean; setShowNextButton: Dispatch>; + userId: string; }; type CloseEndedData = { @@ -29,6 +36,7 @@ const CloseEndedTool = ({ setSatisfiedConditions, showNextButton, setShowNextButton, + userId, }: CloseEndedToolProps) => { const [disable, setDisable] = useState(false); const [assessment, setAssessment] = useState(); @@ -40,6 +48,34 @@ const CloseEndedTool = ({ setDisable(false); setAssessment(''); //to move in validation button + if (userId && actualActivity?._id) { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'openNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + return () => { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'closeNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + }; + } }, [actualActivity]); const toast = useToast(); if (!isOpen) return <>; diff --git a/src/components/ActivityTypes/multichoiceQuestion.tsx b/src/components/ActivityTypes/multichoiceQuestion.tsx index 5e6e669..18d495b 100644 --- a/src/components/ActivityTypes/multichoiceQuestion.tsx +++ b/src/components/ActivityTypes/multichoiceQuestion.tsx @@ -16,7 +16,13 @@ import { useEffect, useState, } from 'react'; -import { PolyglotNodeValidation } from '../../types/polyglotElements'; +import { API } from '../../data/api'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + ZoneId, +} from '../../types/polyglotElements'; import FlexText from '../CostumTypography/FlexText'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; @@ -27,6 +33,7 @@ type MultichoiceToolProps = { setSatisfiedConditions: Dispatch>; showNextButton: boolean; setShowNextButton: Dispatch>; + userId: string; }; type MultichoiceQuestionData = { @@ -42,6 +49,7 @@ const MultichoiceTool = ({ setSatisfiedConditions, showNextButton, setShowNextButton, + userId, }: MultichoiceToolProps) => { const [disable, setDisable] = useState(false); const data = actualActivity?.data as MultichoiceQuestionData; @@ -55,7 +63,34 @@ const MultichoiceTool = ({ if (!data) return; setDisable(false); setCheckBoxValue(''); - //to move in validation button + if (userId && actualActivity?._id) { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'openNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + return () => { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'closeNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + }; + } }, [actualActivity]); const toast = useToast(); diff --git a/src/components/ActivityTypes/openQuestion.tsx b/src/components/ActivityTypes/openQuestion.tsx index af376c5..70e555d 100644 --- a/src/components/ActivityTypes/openQuestion.tsx +++ b/src/components/ActivityTypes/openQuestion.tsx @@ -12,7 +12,13 @@ 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, + ZoneId, +} from '../../types/polyglotElements'; + import FlexText from '../CostumTypography/FlexText'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; @@ -24,6 +30,7 @@ type OpenQuestionToolProps = { setSatisfiedConditions: Dispatch>; showNextButton: boolean; setShowNextButton: Dispatch>; + userId: string; }; type OpenQuestionData = { @@ -41,6 +48,7 @@ const OpenQuestionTool = ({ setSatisfiedConditions, showNextButton, setShowNextButton, + userId, }: OpenQuestionToolProps) => { const [isDisable, setDisable] = useState(false); const [isLoading, setLoading] = useState(false); @@ -53,6 +61,34 @@ const OpenQuestionTool = ({ setDisable(false); setAssessment(''); //to move in validation button + if (userId && actualActivity?._id) { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'openNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + return () => { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'closeNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + }; + } }, [actualActivity]); const toast = useToast(); if (!isOpen) return <>; diff --git a/src/components/ActivityTypes/readMaterial.tsx b/src/components/ActivityTypes/readMaterial.tsx index 9c6ea2c..9a5e126 100644 --- a/src/components/ActivityTypes/readMaterial.tsx +++ b/src/components/ActivityTypes/readMaterial.tsx @@ -1,7 +1,12 @@ import { Box, Flex, Link } from '@chakra-ui/react'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; 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 +17,7 @@ type ReadMaterialToolProps = { unlock: Dispatch>; setSatisfiedConditions: Dispatch>; showNextButton: boolean; + userId: string; }; type ReadMaterialData = { @@ -24,6 +30,7 @@ const ReadMaterialTool = ({ actualActivity, unlock, setSatisfiedConditions, + userId, }: ReadMaterialToolProps) => { const [pdfUrl, setPdfUrl] = useState(null); @@ -48,6 +55,35 @@ const ReadMaterialTool = ({ } }; fetchPdf(); + + if (userId && actualActivity?._id) { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'openNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + return () => { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'closeNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + }; + } }, [actualActivity]); useEffect(() => { diff --git a/src/components/ActivityTypes/summary.tsx b/src/components/ActivityTypes/summary.tsx index 2f1ba51..ea1bbd9 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 { API } from '../../data/api'; +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,7 @@ type SummaryToolProps = { unlock: Dispatch>; setSatisfiedConditions: Dispatch>; showNextButton: boolean; + userId: string; }; type SummaryData = { @@ -25,6 +32,7 @@ const SummaryTool = ({ actualActivity, unlock, setSatisfiedConditions, + userId, }: SummaryToolProps) => { const [summary, setSummary] = useState(''); const { onCopy } = useClipboard(summary || ''); @@ -37,6 +45,34 @@ const SummaryTool = ({ unlock(true); const edgesId = actualActivity?.validation.map((edge) => edge.id); if (edgesId != undefined) setSatisfiedConditions(edgesId); + if (userId && actualActivity?._id) { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'openNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + return () => { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'closeNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + }; + } }, [actualActivity]); if (!isOpen) return <>; diff --git a/src/components/ActivityTypes/trueFalse.tsx b/src/components/ActivityTypes/trueFalse.tsx index 6fcf51c..90c9eb0 100644 --- a/src/components/ActivityTypes/trueFalse.tsx +++ b/src/components/ActivityTypes/trueFalse.tsx @@ -10,10 +10,17 @@ import { useToast, } from '@chakra-ui/react'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import { PolyglotNodeValidation } from '../../types/polyglotElements'; +import { API } from '../../data/api'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + 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 +28,7 @@ type TrueFalseToolProps = { setSatisfiedConditions: Dispatch>; showNextButton: boolean; setShowNextButton: Dispatch>; + userId: string; }; type TrueFalseData = { @@ -38,6 +46,7 @@ const TrueFalseTool = ({ setSatisfiedConditions, showNextButton, setShowNextButton, + userId, }: TrueFalseToolProps) => { const [disable, setDisable] = useState(false); const data = actualActivity?.data as TrueFalseData; @@ -51,6 +60,34 @@ const TrueFalseTool = ({ for (let i = 0; i < max; i++) setup.push('true'); setRadioValue(setup); //to move in validation button + if (userId && actualActivity?._id) { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'openNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + return () => { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'closeNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + }; + } }, [actualActivity]); const toast = useToast(); diff --git a/src/components/ActivityTypes/watchVideo.tsx b/src/components/ActivityTypes/watchVideo.tsx index 20c96d6..33de3c5 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 { API } from '../../data/api'; +import { + OpenCloseNodeAction, + Platform, + PolyglotNodeValidation, + ZoneId, +} from '../../types/polyglotElements'; import HeadingSubtitle from '../CostumTypography/HeadingSubtitle'; import HeadingTitle from '../CostumTypography/HeadingTitle'; type WatchVideoToolProps = { @@ -9,6 +15,7 @@ type WatchVideoToolProps = { actualActivity: PolyglotNodeValidation | undefined; unlock: Dispatch>; setSatisfiedConditions: Dispatch>; + userId: string; }; type WatchVideoData = { @@ -20,6 +27,7 @@ const WatchVideoTool = ({ actualActivity, unlock, setSatisfiedConditions, + userId, }: WatchVideoToolProps) => { if (!isOpen) return <>; console.log('data check ' + actualActivity); @@ -38,6 +46,34 @@ const WatchVideoTool = ({ unlock(true); const edgesId = actualActivity?.validation.map((edge) => edge.id); if (edgesId != undefined) setSatisfiedConditions(edgesId); + if (userId && actualActivity?._id) { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'openNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + return () => { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'closeNodeAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: { + flowId: actualActivity?.flowId, + nodeId: actualActivity?._id, + activity: 'ReadMaterial', + }, + } as OpenCloseNodeAction); + }; + } }, [actualActivity]); return ( diff --git a/src/data/api.ts b/src/data/api.ts index 01acaf0..4e18ef8 100644 --- a/src/data/api.ts +++ b/src/data/api.ts @@ -1,5 +1,6 @@ import axiosCreate, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; import { + AnalyticsActionBody, ManualProgressInfo, PolyglotFlow, ProgressInfo, @@ -131,4 +132,10 @@ export const API = { } ); }, + //register action + registerAction: ( + body: AnalyticsActionBody /*NextBody*/ + ): Promise => { + return axios.post<{}, AxiosResponse, {}>(`/api/learningAnalytics/`, body); + }, }; diff --git a/src/pages/gamifiedUI/index.tsx b/src/pages/gamifiedUI/index.tsx new file mode 100644 index 0000000..f218c15 --- /dev/null +++ b/src/pages/gamifiedUI/index.tsx @@ -0,0 +1,114 @@ +/* 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'; + +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..a5892d7 100644 --- a/src/pages/tools/[id]/index.tsx +++ b/src/pages/tools/[id]/index.tsx @@ -22,7 +22,11 @@ import TrueFalseTool from '../../../components/ActivityTypes/trueFalse'; import WatchVideoTool from '../../../components/ActivityTypes/watchVideo'; import Navbar from '../../../components/NavBars/NavBar'; import { API } from '../../../data/api'; -import { PolyglotNodeValidation } from '../../../types/polyglotElements'; +import { + Platform, + PolyglotNodeValidation, + ZoneId, +} from '../../../types/polyglotElements'; import auth0 from '../../../utils/auth0'; const FlowIndex = () => { @@ -34,6 +38,8 @@ const FlowIndex = () => { const router = useRouter(); const ctx = router.query?.id?.toString(); const [showNextButton, setShowNextButton] = useState(false); + const [scriptCheck, setScriptCheck] = useState(false); + const [userId, setUserId] = useState(''); useEffect(() => { if (ctx != undefined) @@ -46,12 +52,40 @@ const FlowIndex = () => { 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; + setUserId(WA.player.playerId.toString()); + if (userId) { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'openToolAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: undefined, + }); + return () => { + API.registerAction({ + timestamp: new Date(), + userId: userId, + actionType: 'closeToolAction', + zoneId: ZoneId.WebAppZone, + platform: Platform.WebApp, + action: undefined, + }); + }; + } + }, [scriptCheck]); return ( @@ -89,12 +123,14 @@ const FlowIndex = () => { unlock={setUnlock} setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} + userId={userId} /> { setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} setShowNextButton={setShowNextButton} + userId={userId} /> { setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} setShowNextButton={setShowNextButton} + userId={userId} /> { setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} setShowNextButton={setShowNextButton} + userId={userId} /> { setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} setShowNextButton={setShowNextButton} + userId={userId} /> { unlock={setUnlock} setSatisfiedConditions={setSatisfiedConditions} showNextButton={showNextButton} + userId={userId} />