Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions public/demo-html-trrack/assets/dots-count.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@


// Initialize Trrack
const trrack = initializeTrrack({
const trrack = Revisit.createTrrack({
initializeTrrack,
initialState,
registry
});
Expand Down Expand Up @@ -182,7 +183,6 @@
Revisit.postAnswers({
[taskId]: dots.length
});
Revisit.postProvenance(trrack.graph.backend);
}

// Register a listener to update the dot chart based on trrack updates.
Expand Down
4 changes: 2 additions & 2 deletions public/demo-svelte-trrack/assets/dots-count.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@
}
);

const trrack = initializeTrrack({
const trrack = Revisit.createTrrack({
initializeTrrack,
initialState,
registry
});
Expand Down Expand Up @@ -128,7 +129,6 @@
Revisit.postAnswers({
[taskId]: dots.length
});
Revisit.postProvenance(trrack.graph.backend);
}

trrack.currentChange(updateRevisit);
Expand Down
73 changes: 73 additions & 0 deletions public/revisitUtilities/revisit-communicate.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,48 @@
let onDataReceiveCallback = null;
let onProvenanceReceiveCallback = null;
let onAnswerReceiveCallback = null;
const managedTrrackSubscriptions = new Set();

const appendTraversalEvent = (previousProvenance, incomingProvenance, observedAt) => {
const isSameGraph = previousProvenance?.root === incomingProvenance.root;
const previousEvents = isSameGraph && Array.isArray(previousProvenance?.traversalEvents)
? previousProvenance.traversalEvents
: [];
const incomingEvents = Array.isArray(incomingProvenance.traversalEvents)
? incomingProvenance.traversalEvents
: [];
const traversalEvents = incomingEvents.length >= previousEvents.length
? incomingEvents
: previousEvents;
const currentNodeId = incomingProvenance.current;
const currentNode = incomingProvenance.nodes?.[currentNodeId];

if (!currentNode || traversalEvents.at(-1)?.nodeId === currentNodeId) {
return traversalEvents.length > 0
? { ...incomingProvenance, traversalEvents }
: incomingProvenance;
}

const isNewNode = !isSameGraph || !previousProvenance?.nodes?.[currentNodeId];
const eventTime = isNewNode ? currentNode.createdOn : observedAt;
const previousEventTime = traversalEvents.at(-1)?.createdOn ?? Number.NEGATIVE_INFINITY;

return {
...incomingProvenance,
traversalEvents: [
...traversalEvents,
{
nodeId: currentNodeId,
createdOn: Math.max(eventTime, previousEventTime + 1),
},
],
};
};

const disposeManagedTrracks = () => {
managedTrrackSubscriptions.forEach((unsubscribe) => unsubscribe());
managedTrrackSubscriptions.clear();
};

window.addEventListener('message', function (e) {
const data = e.data;
Expand All @@ -41,9 +83,35 @@
postAnswers: (answers) => {
sendMessage("ANSWERS", answers);
},
/**
* @deprecated Use Revisit.createTrrack so every traversal is reported automatically.
*/
postProvenance: (provenance) => {
sendMessage("PROVENANCE", provenance);
},
createTrrack: ({ initializeTrrack, ...options }) => {
if (typeof initializeTrrack !== 'function') {
throw new TypeError('Revisit.createTrrack requires the Trrack initializeTrrack function.');
}

const trrack = initializeTrrack(options);
let previousProvenance;
const publishProvenance = () => {
const provenance = appendTraversalEvent(
previousProvenance,
trrack.graph.backend,
Date.now(),
);
previousProvenance = provenance;
sendMessage("PROVENANCE", provenance);
};
const unsubscribe = trrack.currentChange(publishProvenance);

managedTrrackSubscriptions.add(unsubscribe);
publishProvenance();

return trrack;
},
postEvent: (eventName, objectId) => {
sendMessage("EVENT", { eventName, objectId });
},
Expand Down Expand Up @@ -72,4 +140,9 @@
},
false
);
window.addEventListener('pagehide', (event) => {
if (!event.persisted) {
disposeManagedTrracks();
}
});
})();
50 changes: 31 additions & 19 deletions src/components/audioAnalysis/provenanceReplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,15 @@ export type ProvenanceReplaySelection = ProvenanceTraversalEvent & {
fromTraversal: boolean;
};

export function getReplaySelection(
/**
* @deprecated Graph creation order cannot reproduce undo, redo, or arbitrary
* traversal. Retained only for provenance recorded before traversal events.
*/
function getLegacyReplaySelection(
provenance: TrrackedProvenance,
playTime: number,
currentNode?: string | null,
): ProvenanceReplaySelection {
const traversalEvents = getProvenanceTraversalEvents(provenance);

if (traversalEvents.length > 0) {
let replaySelection = {
nodeId: provenance.root as string,
createdOn: provenance.nodes[provenance.root].createdOn,
fromTraversal: true,
};

traversalEvents.forEach((event) => {
if (event.createdOn <= playTime) {
replaySelection = { ...event, fromTraversal: true };
}
});

return replaySelection;
}

if (!currentNode || !provenance.nodes[currentNode]) {
return {
nodeId: provenance.root as string,
Expand Down Expand Up @@ -65,6 +51,32 @@ export function getReplaySelection(
};
}

export function getReplaySelection(
provenance: TrrackedProvenance,
playTime: number,
currentNode?: string | null,
): ProvenanceReplaySelection {
const traversalEvents = getProvenanceTraversalEvents(provenance);

if (traversalEvents.length > 0) {
let replaySelection = {
nodeId: provenance.root as string,
createdOn: provenance.nodes[provenance.root].createdOn,
fromTraversal: true,
};

traversalEvents.forEach((event) => {
if (event.createdOn <= playTime) {
replaySelection = { ...event, fromTraversal: true };
}
});

return replaySelection;
}

return getLegacyReplaySelection(provenance, playTime, currentNode);
}

export function getReplayNodeId(
provenance: TrrackedProvenance,
playTime: number,
Expand Down
41 changes: 23 additions & 18 deletions src/components/response/ResponseBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, {
import isEqual from 'lodash.isequal';
import { IconAlertTriangle } from '@tabler/icons-react';
import { useNavigate } from 'react-router';
import { Registry, initializeTrrack } from '@trrack/core';
import { Registry } from '@trrack/core';
import {
IndividualComponent,
ResponseBlockLocation,
Expand All @@ -33,7 +33,7 @@ import { shouldUseStimulusValidation } from './stimulusErrors';
import { ResponseSwitcher } from './ResponseSwitcher';
import { FeedbackAlert } from './FeedbackAlert';
import {
CustomResponseField, FormElementProvenance, StoredAnswer,
CustomResponseField, FormElementProvenance, StoredAnswer, TrrackedProvenance,
} from '../../store/types';
import { useStudyConfig } from '../../store/hooks/useStudyConfig';
import { useStoredAnswer } from '../../store/hooks/useStoredAnswer';
Expand All @@ -42,6 +42,7 @@ import { useIsAnalysis } from '../../store/hooks/useIsAnalysis';
import { responseAnswerIsCorrect } from '../../utils/correctAnswer';
import { getCustomResponseModule, getCustomResponseModuleLoadError } from './customResponseModules';
import { appendStimulusShowErrorsToGraph } from './stimulusProvenance';
import { useManagedTrrack } from '../../store/hooks/useRevisitTrrack';
import { useStorageEngine } from '../../storage/storageEngineHooks';
import { showNotification } from '../../utils/notifications';
import { getAnswersFromAllLocations } from '../../utils/getAnswersFromAllLocations';
Expand Down Expand Up @@ -71,10 +72,12 @@ export function ResponseBlock({
}: Props) {
const storeDispatch = useStoreDispatch();
const {
updateResponseBlockValidation, saveIncorrectAnswer, saveTrialAnswer, setResponseSubmitAttempt, setStimulusSubmitAttempt, setCheckAnswerResult,
updateProvenance, updateResponseBlockValidation, saveIncorrectAnswer, saveTrialAnswer, setResponseSubmitAttempt, setStimulusSubmitAttempt, setCheckAnswerResult,
} = useStoreActions();

const currentStep = useCurrentStep();
const identifier = useCurrentIdentifier();
const isAnalysis = useIsAnalysis();
const currentProvenance = useStoreSelector((state) => state.analysisProvState[location]) as FormElementProvenance | undefined;

const storedAnswer = useMemo(() => currentProvenance?.form || status?.answer, [currentProvenance, status]);
Expand Down Expand Up @@ -113,7 +116,7 @@ export function ResponseBlock({
}), [allResponses]);

// Set up trrack to store provenance graph of the answerValidator status
const { actions, trrack } = useMemo(() => {
const { actions, registry } = useMemo(() => {
const reg = Registry.create();

const updateFormAction = reg.register('update', (state, payload: StoredAnswer['answer']) => {
Expand All @@ -125,22 +128,29 @@ export function ResponseBlock({
return state;
});

const trrackInst = initializeTrrack({
registry: reg,
initialState: {
form: null,
showResponseErrors: false,
},
});

return {
actions: {
updateFormAction,
updateShowResponseErrorsAction,
},
trrack: trrackInst,
registry: reg,
};
}, []);
const reportProvenance = useCallback((provenanceGraph: TrrackedProvenance) => {
if (isAnalysis) return;
storeDispatch(updateProvenance({
location,
identifier,
provenanceGraph,
}));
}, [identifier, isAnalysis, location, storeDispatch, updateProvenance]);
const trrack = useManagedTrrack({
registry,
initialState: {
form: null,
showResponseErrors: false,
},
}, reportProvenance, identifier);

const reactiveAnswers = useStoreSelector((state) => state.reactiveAnswers);

Expand All @@ -150,15 +160,13 @@ export function ResponseBlock({
const trialValidation = useStoreSelector((state) => state.trialValidation);
const analysisProvState = useStoreSelector((state) => state.analysisProvState);
const { goToNextStep } = useNextStep();
const isAnalysis = useIsAnalysis();

const studyConfig = useStudyConfig();

const provideFeedback = useMemo(() => config?.provideFeedback ?? studyConfig.uiConfig.provideFeedback, [config, studyConfig]);
const hasCorrectAnswerFeedback = !!provideFeedback && ((config?.correctAnswer?.length || 0) > 0);
const allowFailedTraining = useMemo(() => config?.allowFailedTraining ?? studyConfig.uiConfig.allowFailedTraining ?? true, [config, studyConfig]);
const trainingAttempts = useMemo(() => config?.trainingAttempts ?? studyConfig.uiConfig.trainingAttempts ?? 2, [config, studyConfig]);
const identifier = useCurrentIdentifier();
const savedSubmitAttempt = storedAnswerData?.responseSubmitAttempted ?? status?.responseSubmitAttempted ?? false;
const currentSubmitAttempt = useStoreSelector((state) => state.responseSubmitAttempted[identifier]);
const liveErrors = currentSubmitAttempt ?? savedSubmitAttempt;
Expand Down Expand Up @@ -395,7 +403,6 @@ export function ResponseBlock({
identifier,
status: answerValidator.isValid(),
values: structuredClone(answerValidator.values),
provenanceGraph: trrack.graph.backend,
});
}
// Disable exhaustive-deps because we only want this to run when there is a new storedAnswer
Expand Down Expand Up @@ -449,7 +456,6 @@ export function ResponseBlock({
identifier,
status: answerValidator.isValid() || bypassValidationForFailedTraining,
values: structuredClone(answerValidator.values),
provenanceGraph: trrack.graph.backend,
}),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -467,7 +473,6 @@ export function ResponseBlock({
identifier,
status: answerValidator.isValid() || bypassValidationForFailedTraining,
values: structuredClone(answerValidator.values),
provenanceGraph: trrack.graph.backend,
}),
);
}, [actions, answerValidator, bypassValidationForFailedTraining, identifier, isAnalysis, liveErrors, location, storeDispatch, trrack, updateResponseBlockValidation]);
Expand Down
1 change: 1 addition & 0 deletions src/components/response/tests/ResponseBlock.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ vi.mock('@trrack/core', () => ({
},
initializeTrrack: vi.fn(() => ({
apply: vi.fn(),
currentChange: vi.fn(() => vi.fn()),
graph: {
backend: {
root: 'root',
Expand Down
11 changes: 3 additions & 8 deletions src/controllers/IframeController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const PREFIX = '@REVISIT_COMMS';

export function IframeController({ currentConfig, provState, answers }: { currentConfig: WebsiteComponent; provState?: unknown, answers: ParticipantData['answers'] }) {
const {
setReactiveAnswers, updateResponseBlockValidation,
setReactiveAnswers, updateProvenance, updateResponseBlockValidation,
} = useStoreActions();
const storeDispatch = useStoreDispatch();
const dispatch = useDispatch();
Expand Down Expand Up @@ -96,15 +96,10 @@ export function IframeController({ currentConfig, provState, answers }: { curren
break;
case `${PREFIX}/PROVENANCE`: {
if (isAnalysis) return;
const currentStimulusValidation = stimulusValidationRef.current;
storeDispatch(updateResponseBlockValidation({
storeDispatch(updateProvenance({
location: 'stimulus',
identifier,
values: {},
status: currentStimulusValidation?.valid ?? true,
provenanceGraph: data.message,
reason: currentStimulusValidation?.reason,
message: currentStimulusValidation?.message,
}));
break;
}
Expand All @@ -117,7 +112,7 @@ export function IframeController({ currentConfig, provState, answers }: { curren
window.addEventListener('message', handler);

return () => window.removeEventListener('message', handler);
}, [storeDispatch, dispatch, iframeId, currentConfig, sendMessage, setReactiveAnswers, updateResponseBlockValidation, identifier, isAnalysis, provState, answers]);
}, [storeDispatch, dispatch, iframeId, currentConfig, sendMessage, setReactiveAnswers, updateProvenance, updateResponseBlockValidation, identifier, isAnalysis, provState, answers]);

return (
<iframe
Expand Down
Loading
Loading