diff --git a/@empirica-mocks/core/mocks.js b/@empirica-mocks/core/mocks.js index 48412c3..7dc3886 100644 --- a/@empirica-mocks/core/mocks.js +++ b/@empirica-mocks/core/mocks.js @@ -79,6 +79,8 @@ export function useStage() { setTreatment, templatesMap, setTemplatesMap, + refData, + setRefData, selectedTreatmentIndex, setSelectedTreatmentIndex } = useContext(StageContext) @@ -92,23 +94,127 @@ export function useStage() { //const treatmentString = localStorage.getItem("treatment"); //const treatment = JSON.parse(treatmentString); - if (varName === "elements") { - let elements = treatment.treatments[selectedTreatmentIndex]?.gameStages[currentStageIndex]?.elements; - if (Array.isArray(elements)) { - elements = elements.flatMap((element) => { - if (element.template) { - return templatesMap.get(element.template); + var tempStage = null; // for template stages + const stageTemplateName = treatment.treatments[0]?.gameStages[currentStageIndex]?.template || ""; + var fields = treatment.treatments[0]?.gameStages[currentStageIndex]?.fields || []; + if (stageTemplateName !== "") { + tempStage = templatesMap.get(stageTemplateName)[0] + } + console.log("tempStage", tempStage); + + //logic to fill in ${} props + // move logic outside get() + const variablePattern = /\${([^}]+)}/; + {tempStage && + tempStage.elements.forEach(element => { + Object.keys(element).forEach(key => { + const value = element[key]; + + if (typeof value === "string" && variablePattern.test(value)) { + const match = value.match(variablePattern); + if (match) { + console.log("replaced " + match[1] + " with " + fields[match[1]]); + element[key] = fields[match[1]]; + } } - return [element]; }); + }); + } + + if (varName === "elements") { + // MAIN ==> + // let elements = treatment.treatments[selectedTreatmentIndex]?.gameStages[currentStageIndex]?.elements; + // if (Array.isArray(elements)) { + // elements = elements.flatMap((element) => { + // if (element.template) { + // return templatesMap.get(element.template); + // } + // return [element]; + // }); + // } else { + // elements = []; + // } + // console.log("revised elements", elements) + // return elements; + + var elements + if (tempStage) { + elements = tempStage.elements; } else { - elements = []; + elements = treatment.treatments[0]?.gameStages[currentStageIndex]?.elements; } - console.log("revised elements", elements) + + console.log("CURRELEMENTS", elements) + + // TODO: change to template if needed + // map to templates first + elements = elements.flatMap((element) => { + if (element.template) { + return templatesMap.get(element.template); + } else { + return element; + } + }) + + //console.log("ELEMENTS_TO_DISPLAY", elements) + // check all conditions + elements = elements.flatMap((element) => { + if (element.conditions) { + // TODO: update with other comparators + const conditions = element.conditions; + const comparator = conditions[0]?.comparator || "x"; + const reference = conditions[0]?.reference || "x"; + const value = conditions[0]?.value || "x"; + if (comparator === "x") { + return [element]; + } else if (comparator === "exists") { + if (refData[`stage_${currentStageIndex}`]?.[reference]) { + const newElement = {...element}; + delete newElement.conditions; + return [newElement]; + } else { + return []; + } + } else if (comparator === "equals") { + if (refData[`stage_${currentStageIndex}`]?.[reference] == value) { + const newElement = {...element}; + delete newElement.conditions; + return [newElement]; + } else { + return []; + } + } else if (comparator === "doesNotEqual") { + if (refData[`stage_${currentStageIndex}`]?.[reference] != value) { + const newElement = {...element}; + delete newElement.conditions; + return [newElement]; + } else { + return []; + } + } + + const condition = conditions.find((condition) => { + if (condition.field) { + return fields[condition.field] === condition.value; + } + return true; + }); + if (condition) { + return [element]; + } + } + return [element]; + }); return elements; } else if (varName === "discussion") { + if (tempStage) { + return tempStage.discussion || []; + } return treatment.treatments[selectedTreatmentIndex]?.gameStages[currentStageIndex]?.discussion; } else if (varName === "name") { + if (tempStage) { + return tempStage.name; + } return treatment.treatments[selectedTreatmentIndex]?.gameStages[currentStageIndex]?.name; } else if (varName === "index") { return currentStageIndex; diff --git a/cypress/downloads/downloads.html b/cypress/downloads/downloads.html new file mode 100644 index 0000000..a73e074 Binary files /dev/null and b/cypress/downloads/downloads.html differ diff --git a/cypress/fixtures/exampleTreatment.yaml b/cypress/fixtures/exampleTreatment.yaml new file mode 100644 index 0000000..7f99958 --- /dev/null +++ b/cypress/fixtures/exampleTreatment.yaml @@ -0,0 +1,43 @@ +templates: + - templateName: testA + templateContent: + - type: prompt + file: projects/example/multipleChoiceColors.md + - type: submitButton + buttonText: Finish Stage 1 + conditions: + - comparator: exists + reference: prompt.Colors + - templateName: testB + templateContent: + - name: GameStageTemplate + duration: 200 + elements: + - type: prompt + file: projects/example/multipleChoiceColors.md + - type: submitButton + buttonText: ${submitButtonText} +treatments: + - name: simple template test + playerCount: 1 + gameStages: + - name: TestTemplateA + duration: 150 + elements: + - template: testA + - name: TestStage + duration: 300 + elements: + - name: ExitTicket + type: prompt + file: projects/example/multipleChoiceColors.md + - name: submitButton + type: submitButton + buttonText: Finish Stage 2 + conditions: + - comparator: doesNotEqual + reference: prompt.ExitTicket + value: 1 + - template: testB + fields: + submitButtonText: Finish Stage 3 diff --git a/deliberation-empirica b/deliberation-empirica index 22b49e0..43cef4d 160000 --- a/deliberation-empirica +++ b/deliberation-empirica @@ -1 +1 @@ -Subproject commit 22b49e0bf872a758c8e6ba1fb25960e8e26c6770 +Subproject commit 43cef4d26dd8f59fc7dbc1c69cad351cc0db7f40 diff --git a/src/app/editor/components/CodeEditor.tsx b/src/app/editor/components/CodeEditor.tsx index 7aee9bd..1936bd6 100644 --- a/src/app/editor/components/CodeEditor.tsx +++ b/src/app/editor/components/CodeEditor.tsx @@ -20,7 +20,7 @@ export default function CodeEditor() { async function fetchDefaultTreatment() { var data = defaultTreatment if (defaultTreatment) return // If defaultTreatment is already set, do nothing - + const response = await fetch('/defaultTreatment.yaml') const text = await response.text() data = yaml.load(text) diff --git a/src/app/editor/components/EditElement.tsx b/src/app/editor/components/EditElement.tsx index 3dbe6e8..ce2d527 100644 --- a/src/app/editor/components/EditElement.tsx +++ b/src/app/editor/components/EditElement.tsx @@ -23,9 +23,12 @@ export function EditElement({ templatesMap, setTemplatesMap, selectedTreatmentIndex, - setSelectedTreatmentIndex + setSelectedTreatmentIndex, } = useContext(StageContext) + const stageTemplateName = + treatment.treatments[0]?.gameStages?.[currentStageIndex]?.template || '' + const { register, watch, @@ -35,13 +38,15 @@ export function EditElement({ } = useForm({ defaultValues: { name: - treatment?.treatments?.[selectedTreatmentIndex].gameStages[stageIndex]?.elements[ - elementIndex - ]?.name || '', + stageTemplateName == '' + ? treatment?.treatments[selectedTreatmentIndex].gameStages[stageIndex] + ?.elements?.[elementIndex]?.name || '' + : '', selectedOption: - treatment?.treatments?.[selectedTreatmentIndex].gameStages[stageIndex]?.elements[ - elementIndex - ]?.type || 'Pick one', + stageTemplateName == '' + ? treatment?.treatments[selectedTreatmentIndex].gameStages[stageIndex] + ?.elements?.[elementIndex]?.type || 'Pick one' + : 'Pick one', file: '', url: '', params: [], @@ -90,13 +95,13 @@ export function EditElement({ } if (elementIndex === -1) { - updatedTreatment?.treatments[selectedTreatmentIndex].gameStages[stageIndex]?.elements?.push( - inputs - ) + updatedTreatment?.treatments[selectedTreatmentIndex].gameStages[ + stageIndex + ]?.elements?.push(inputs) } else { - updatedTreatment.treatments[selectedTreatmentIndex].gameStages[stageIndex].elements[ - elementIndex - ] = inputs + updatedTreatment.treatments[selectedTreatmentIndex].gameStages[ + stageIndex + ].elements[elementIndex] = inputs } editTreatment(updatedTreatment) @@ -108,10 +113,9 @@ export function EditElement({ ) if (confirm) { const updatedTreatment = JSON.parse(JSON.stringify(treatment)) // deep copy - updatedTreatment.treatments[selectedTreatmentIndex].gameStages[stageIndex].elements.splice( - elementIndex, - 1 - ) // delete in place + updatedTreatment.treatments[selectedTreatmentIndex].gameStages[ + stageIndex + ].elements.splice(elementIndex, 1) // delete in place editTreatment(updatedTreatment) } } diff --git a/src/app/editor/components/ElementCard.tsx b/src/app/editor/components/ElementCard.tsx index 4a56db9..144d839 100644 --- a/src/app/editor/components/ElementCard.tsx +++ b/src/app/editor/components/ElementCard.tsx @@ -1,4 +1,4 @@ -import React, { useState, useContext } from 'react' +import React, { useState, useContext, useEffect } from 'react' import { Modal } from './Modal' import { EditElement } from './EditElement' import { TreatmentType } from '../../../../deliberation-empirica/server/src/preFlight/validateTreatmentFile' @@ -12,6 +12,7 @@ export function ElementCard({ stageIndex, elementIndex, elementOptions, + isTemplate, }: { element: any scale: number @@ -20,11 +21,20 @@ export function ElementCard({ stageIndex: number elementIndex: number elementOptions: any + isTemplate: boolean }) { const startTime = element.displayTime || 0 const endTime = element.hideTime || stageDuration const [modalOpen, setModalOpen] = useState(false) + const [isElementTemplate, setIsElementTemplate] = useState(false) + + useEffect(() => { + if (element.template) { + setIsElementTemplate(true) + } + }, [element]) + const { currentStageIndex, setCurrentStageIndex, @@ -47,24 +57,30 @@ export function ElementCard({ tabIndex={0} >
- {Object.keys(element).map((key) => ( -

- {key}: {element[key]} -

- ))} + {Object.keys(element).map((key) => { + const value = element[key] + return ( +

+ {key}: {typeof value === 'object' ? JSON.stringify(value) : value} +

+ ) + })}
- + + {!isElementTemplate && !isTemplate && ( + + )} diff --git a/src/app/editor/components/ReferenceData.tsx b/src/app/editor/components/ReferenceData.tsx index 1f27d31..66ff515 100644 --- a/src/app/editor/components/ReferenceData.tsx +++ b/src/app/editor/components/ReferenceData.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useContext } from 'react' +import { StageContext } from '@/editor/stageContext' // helper to format references const formatReference = (reference: string) => { @@ -15,15 +16,25 @@ const getPlaceholderText = (reference: string) => { } // find 'references' in the treatment object by stage (recursively..hopefully runtime not too bad) -const findReferencesByStage = (obj: any): any[] => { +const findReferencesByStage = (obj: any, templatesMap: any): any[] => { let references: any[] = [] if (typeof obj === 'object' && obj !== null) { for (const key in obj) { if (key === 'reference') { references.push(obj[key]) + } else if (key === 'template') { + const templateReferences = templatesMap + .get(obj[key]) + .flatMap((templateElement: any) => + findReferencesByStage(templateElement, templatesMap) + ) + + references = references.concat(templateReferences) } else if (typeof obj[key] === 'object') { - references = references.concat(findReferencesByStage(obj[key])) + references = references.concat( + findReferencesByStage(obj[key], templatesMap) + ) } } } @@ -32,10 +43,10 @@ const findReferencesByStage = (obj: any): any[] => { } // initializing json data for each stage -const initializeJsonData = (treatment: any) => { +const initializeJsonData = (treatment: any, templatesMap: any) => { const jsonData: { [key: string]: any } = {} treatment?.gameStages?.forEach((stage: any, index: number) => { - const references = findReferencesByStage(stage) + const references = findReferencesByStage(stage, templatesMap) jsonData[`stage_${index}`] = {} references.forEach((reference) => { jsonData[`stage_${index}`][reference] = '' @@ -70,16 +81,18 @@ const ReferenceData = ({ treatment, stageIndex }: ReferenceDataProps) => { const [jsonData, setJsonData] = useState({}) const [inputValues, setInputValues] = useState({}) + const { refData, setRefData, templatesMap } = useContext(StageContext) + // load refs for curr stage useEffect(() => { if (treatment?.gameStages?.[stageIndex]) { const stage = treatment.gameStages[stageIndex] - const allReferences = findReferencesByStage(stage) + const allReferences = findReferencesByStage(stage, templatesMap) setReferences(allReferences) // load json data for curr stage if (!jsonData[`stage_${stageIndex}`]) { - const initializedJson = initializeJsonData(treatment) + const initializedJson = initializeJsonData(treatment, templatesMap) setJsonData((prev) => ({ ...prev, ...initializedJson })) } @@ -123,6 +136,7 @@ const ReferenceData = ({ treatment, stageIndex }: ReferenceDataProps) => { [`stage_${stageIndex}`]: inputValues[`stage_${stageIndex}`], } setJsonData(updatedJson) + setRefData(updatedJson) localStorage.setItem('jsonData', JSON.stringify(updatedJson)) console.log('Saved JSON Data:', JSON.stringify(updatedJson, null, 2)) } @@ -134,6 +148,7 @@ const ReferenceData = ({ treatment, stageIndex }: ReferenceDataProps) => { if (savedJson) { console.log('Loaded JSON Data from localStorage:', JSON.parse(savedJson)) setJsonData(JSON.parse(savedJson)) + setRefData(JSON.parse(savedJson)) } if (savedInputValues) { diff --git a/src/app/editor/components/StageCard.tsx b/src/app/editor/components/StageCard.tsx index 2e527fa..be7882b 100644 --- a/src/app/editor/components/StageCard.tsx +++ b/src/app/editor/components/StageCard.tsx @@ -22,6 +22,7 @@ export function StageCard({ sequence, stageIndex, setRenderPanelStage, + isTemplate, }: { title: string elements: any[] @@ -30,6 +31,7 @@ export function StageCard({ sequence: string stageIndex: number setRenderPanelStage: any + isTemplate: boolean }) { const { currentStageIndex, @@ -100,8 +102,9 @@ export function StageCard({ // update treatment const updatedTreatment = JSON.parse(JSON.stringify(treatment)) - updatedTreatment.treatments[selectedTreatmentIndex].gameStages[stageIndex].elements = - updatedElements + updatedTreatment.treatments[selectedTreatmentIndex].gameStages[ + stageIndex + ].elements = updatedElements editTreatment(updatedTreatment) } @@ -121,20 +124,22 @@ export function StageCard({ >

{title}

- + {!isTemplate && ( + + )} @@ -154,32 +159,51 @@ export function StageCard({ {...provided.droppableProps} > {elements !== undefined && - elements.map((element, index) => ( - - {(provided) => ( -
- -
- )} -
- ))} + elements.map((element, index) => + isTemplate ? ( +
+ +
+ ) : ( + + {(provided) => ( +
+ +
+ )} +
+ ) + )} {provided.placeholder}
)} @@ -187,25 +211,27 @@ export function StageCard({ {/* Add Element Button*/} -
- + {!isTemplate && ( +
+ - - - -
+ + + +
+ )} ) } diff --git a/src/app/editor/components/Timeline.tsx b/src/app/editor/components/Timeline.tsx index e203253..17d1af0 100644 --- a/src/app/editor/components/Timeline.tsx +++ b/src/app/editor/components/Timeline.tsx @@ -8,6 +8,11 @@ import { EditStage } from './EditStage' import { StageContext } from '@/editor/stageContext' import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd' import Dropdown from './Dropdown' +import { stringify } from 'yaml' +import { + treatmentSchema, + TreatmentType, +} from '../../../../deliberation-empirica/server/src/preFlight/validateTreatmentFile' export default function Timeline({ setRenderPanelStage, @@ -39,20 +44,39 @@ export default function Timeline({ useEffect(() => { // Access localStorage only on the client side if (typeof window !== 'undefined') { - const codeStr = localStorage.getItem('code') || '' + let codeStr = localStorage.getItem('code') || '' + //codeStr = '' // In case anything goes wrong, use to reset const parsedCode = parse(codeStr) setTreatment(parsedCode) const storedFilter = localStorage.getItem('currentStageName') || 'all' setCurrentStageName(storedFilter) - const storedTreatmentIndex = - parseInt(localStorage.getItem('selectedTreatmentIndex') || '0', 10) + const storedTreatmentIndex = parseInt( + localStorage.getItem('selectedTreatmentIndex') || '0', + 10 + ) setSelectedTreatmentIndex(storedTreatmentIndex) - const storedIntroSequenceIndex = - parseInt(localStorage.getItem('selectedIntroSequenceIndex') || '0', 10) + const storedIntroSequenceIndex = parseInt( + localStorage.getItem('selectedIntroSequenceIndex') || '0', + 10 + ) setSelectedIntroSequenceIndex(storedIntroSequenceIndex) + + if (parsedCode && parsedCode.treatments?.[0].gameStages) { + const stageNames = parsedCode.treatments[0].gameStages.map( + (stage: any) => stage.name + ) + setIntroSequenceOptions(['all', ...stageNames]) // 'all' as default + } + if (parsedCode?.templates) { + const templates = new Map() + parsedCode.templates.forEach((template: any) => { + templates.set(template.templateName, template.templateContent) + }) + setTemplatesMap(templates) + } } }, [setTreatment, setSelectedTreatmentIndex, setSelectedIntroSequenceIndex]) @@ -150,7 +174,9 @@ export default function Timeline({ localStorage.setItem('currentStageName', 'all') } - function handleIntroSequenceChange(event: React.ChangeEvent) { + function handleIntroSequenceChange( + event: React.ChangeEvent + ) { const newIndex = parseInt(event.target.value, 10) setSelectedIntroSequenceIndex(newIndex) localStorage.setItem('selectedIntroSequenceIndex', newIndex.toString()) @@ -240,15 +266,36 @@ export default function Timeline({ {...provided.draggableProps} {...provided.dragHandleProps} > - + {obj.stage.name && ( + + )} + {obj.stage.template && ( + + )} )} @@ -281,4 +328,4 @@ export default function Timeline({ ) -} \ No newline at end of file +} diff --git a/src/app/editor/stageContext.jsx b/src/app/editor/stageContext.jsx index 2ef8631..2f773f1 100644 --- a/src/app/editor/stageContext.jsx +++ b/src/app/editor/stageContext.jsx @@ -21,49 +21,60 @@ const StageProvider = ({ children }) => { const [elapsed, setElapsed] = useState(0) const [treatment, setTreatment] = useState(null) const [templatesMap, setTemplatesMap] = useState(new Map()) + const [refData, setRefData] = useState({}) const [selectedTreatmentIndex, setSelectedTreatmentIndex] = useState(0) const [selectedIntroSequenceIndex, setSelectedIntroSequenceIndex] = useState(0) const player = usePlayer() // for updating code editor, requires reload - const editTreatment = useCallback((newTreatment) => { - setTreatment(newTreatment) - localStorage.setItem('code', stringify(newTreatment)) - window.location.reload() - }, [setTreatment]) - - const contextValue = useMemo(() => ({ - currentStageIndex, - setCurrentStageIndex, - elapsed, - setElapsed, - treatment, - setTreatment, - editTreatment, - player, - templatesMap, - setTemplatesMap, - selectedTreatmentIndex, - setSelectedTreatmentIndex, - selectedIntroSequenceIndex, - setSelectedIntroSequenceIndex, - }), [ - currentStageIndex, - setCurrentStageIndex, - elapsed, - setElapsed, - treatment, - setTreatment, - editTreatment, - player, - templatesMap, - setTemplatesMap, - selectedTreatmentIndex, - setSelectedTreatmentIndex, - selectedIntroSequenceIndex, - setSelectedIntroSequenceIndex, - ]) + const editTreatment = useCallback( + (newTreatment) => { + setTreatment(newTreatment) + localStorage.setItem('code', stringify(newTreatment)) + window.location.reload() + }, + [setTreatment] + ) + + const contextValue = useMemo( + () => ({ + currentStageIndex, + setCurrentStageIndex, + elapsed, + setElapsed, + treatment, + setTreatment, + editTreatment, + player, + templatesMap, + setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex, + selectedIntroSequenceIndex, + setSelectedIntroSequenceIndex, + refData, + setRefData, + }), + [ + currentStageIndex, + setCurrentStageIndex, + elapsed, + setElapsed, + treatment, + setTreatment, + editTreatment, + player, + templatesMap, + setTemplatesMap, + selectedTreatmentIndex, + setSelectedTreatmentIndex, + selectedIntroSequenceIndex, + setSelectedIntroSequenceIndex, + refData, + setRefData, + ] + ) // expose context values to the window object useEffect(() => {