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(() => {