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
24,880 changes: 24,880 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"test": "playwright test",
"unittest": "vitest",
"find-revisit-users": "bash scripts/find-revisit-users.sh",
"preinstall": "node -e \"if(!/yarn\\.js$/.test(process.env.npm_execpath))throw new Error('Use yarn')\"",
"postinstall": "husky"
},
"lint-staged": {
Expand Down Expand Up @@ -49,7 +48,7 @@
"@types/crypto-js": "^4.2.2",
"@types/hjson": "^2.4.6",
"@types/node": "^24.5.2",
"@visdesignlab/upset2-react": "^1.1.0",
"@visdesignlab/upset2-react": "^1.2.4",
"ajv": "^8.18.0",
"arquero": "^5.4.0",
"crypto-js": "^4.2.0",
Expand All @@ -73,7 +72,7 @@
"react-markdown": "^9.0.3",
"react-redux": "^9.2.0",
"react-router": "^7.12.0",
"react-vega": "^7.6.0",
"react-vega": "^8.0.0",
"recoil": "^0.5.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
Expand All @@ -84,7 +83,7 @@
"use-deep-compare-effect": "^1.8.1",
"uuid": "^14.0.0",
"vega": "^6.2.0",
"vega-lite": "^5.23.0",
"vega-lite": "^6.0.1",
"vite": "^7.3.2",
"wavesurfer-react": "^3.0.4",
"wavesurfer.js": "^7.10.1",
Expand All @@ -105,12 +104,12 @@
"@typescript-eslint/eslint-plugin": "^8.44.0",
"@typescript-eslint/parser": "^8.44.0",
"@vitejs/plugin-react-swc": "^4.1.0",
"eslint": "^9.36.0",
"eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-hooks": "^4.6.2",
"globals": "^16.4.0",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
Expand Down
39 changes: 22 additions & 17 deletions src/analysis/individualStudy/stats/ResponseVisualization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { useDisclosure, useResizeObserver } from '@mantine/hooks';
import {
IconAdjustmentsHorizontal, IconBubbleText, IconChartGridDots, IconChevronDown, IconCodePlus, IconCopyCheck, IconDots, IconGridDots, IconHtml, IconLetterCase, IconDragDrop, IconNumber123, IconRadio, IconSelect, IconSquares,
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { VegaLite, VisualizationSpec } from 'react-vega';
import { useMemo, type ComponentProps } from 'react';
import { VegaEmbed } from 'react-vega';
import { IndividualComponent, ParticipantData, Response } from '../../../parser/types';
import { responseAnswerIsCorrect } from '../../../utils/correctAnswer';

type VegaEmbedSpec = ComponentProps<typeof VegaEmbed>['spec'];

export function ResponseVisualization({
response, participantData, trialId, trialConfig,
}: {
Expand Down Expand Up @@ -254,14 +256,16 @@ export function ResponseVisualization({
<SimpleGrid cols={2} h={360}>
<ScrollArea mih={200}>
{(response.type !== 'metadata' && response.type !== 'shortText' && response.type !== 'longText' && response.type !== 'reactive' && response.type !== 'custom' && response.type !== 'textOnly' && response.type !== 'ranking-sublist' && response.type !== 'ranking-categorical' && response.type !== 'ranking-pairwise') ? (
<VegaLite
spec={vegaLiteSpec as VisualizationSpec}
actions={false}
height={270}
width={(response.type === 'matrix-checkbox' || response.type === 'matrix-radio' ? 500 : (dms.width / 2) - 60 - (correctAnswer === undefined ? 0 : 60))}
padding={0}
style={{ justifySelf: 'center' }}
renderer="svg"
<VegaEmbed
spec={vegaLiteSpec as VegaEmbedSpec}
options={{ actions: false, renderer: 'svg' }}
style={{
justifySelf: 'center',
height: 270,
width: (response.type === 'matrix-checkbox' || response.type === 'matrix-radio'
? 500
: (dms.width / 2) - 60 - (correctAnswer === undefined ? 0 : 60)),
}}
/>
) : (
<Flex direction="column" gap="xs" style={{ overflowX: 'clip' }}>
Expand All @@ -288,13 +292,14 @@ export function ResponseVisualization({
<ScrollArea mih={200}>
{response.type === 'metadata'
? (
<VegaLite
spec={vegaLiteSpec as VisualizationSpec}
actions={false}
width={(dms.width / 2) - 60}
height={270}
padding={0}
style={{ justifySelf: 'center' }}
<VegaEmbed
spec={vegaLiteSpec as VegaEmbedSpec}
options={{ actions: false, renderer: 'svg' }}
style={{
justifySelf: 'center',
width: (dms.width / 2) - 60,
height: 270,
}}
/>
)
: (
Expand Down
10 changes: 5 additions & 5 deletions src/analysis/individualStudy/summary/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ function calculateCorrectnessStats(
visibleParticipants: ParticipantDataWithStatus[],
componentName?: string,
studyConfig?: StudyConfig,
allConfigs: Record<string, StudyConfig> = {},
responseId?: string,
allConfigs: Record<string, StudyConfig> = {},
): { correctness: number; correctCount: number; totalQuestionCount: number } {
// Filter out rejected participants and filter by component if provided
const filteredParticipants = filterParticipants(visibleParticipants, componentName, true);
Expand Down Expand Up @@ -270,7 +270,7 @@ export function getOverviewStats(
avgTime: timeStats.avgTime,
avgCleanTime: timeStats.avgCleanTime,
participantsWithInvalidCleanTimeCount: timeStats.participantsWithInvalidCleanTimeCount,
correctness: calculateCorrectnessStats(visibleParticipants, componentName, studyConfig, allConfigs).correctness,
correctness: calculateCorrectnessStats(visibleParticipants, componentName, studyConfig, undefined, allConfigs).correctness,
};

return overviewData;
Expand Down Expand Up @@ -299,7 +299,7 @@ export function getComponentStats(
totalQuestionCount: number;
}> = componentNames.map((name) => {
const timeStats = calculateTimeStats(visibleParticipants, name);
const correctnessStats = calculateCorrectnessStats(visibleParticipants, name, studyConfig, allConfigs);
const correctnessStats = calculateCorrectnessStats(visibleParticipants, name, studyConfig, undefined, allConfigs);

return {
component: name,
Expand Down Expand Up @@ -405,7 +405,7 @@ export function getResponseStats(
if (responses.length === 0) return [];

return responses.map((response) => {
const correctnessStats = calculateCorrectnessStats(visibleParticipants, name, studyConfig, allConfigs, response.id);
const correctnessStats = calculateCorrectnessStats(visibleParticipants, name, studyConfig, response.id, allConfigs);
return {
responseId: response.id,
component: name,
Expand Down Expand Up @@ -435,7 +435,7 @@ export function getResponseStatsForConfigs(
if (responses.length === 0) return [];

return responses.map((response) => {
const correctnessStats = calculateCorrectnessStats(configParticipants, name, studyConfig, allConfigs, response.id);
const correctnessStats = calculateCorrectnessStats(configParticipants, name, studyConfig, response.id, allConfigs);
return {
responseId: response.id,
component: name,
Expand Down
3 changes: 2 additions & 1 deletion src/components/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) {
participantSession.participantId,
false,
false,
undefined,
participantSession.participantConfigHash !== activeHash,
);

Expand Down Expand Up @@ -367,8 +368,8 @@ export function Shell({ globalConfig }: { globalConfig: GlobalConfig }) {
'',
false,
isStorageFailure,
false,
initialAlertModal,
false,
);

if (isCancelled) {
Expand Down
25 changes: 5 additions & 20 deletions src/components/audioAnalysis/AudioProvenanceVis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useResizeObserver, useThrottledCallback } from '@mantine/hooks';
import { WaveForm, WaveSurfer } from 'wavesurfer-react';
import * as d3 from 'd3';
import {
Registry, Trrack, initializeTrrack, isRootNode,
Registry, Trrack, initializeTrrack,
} from '@trrack/core';
import WaveSurferType from 'wavesurfer.js';
import { useStorageEngine } from '../../storage/storageEngineHooks';
Expand All @@ -26,6 +26,7 @@ import { parseTrialOrder } from '../../utils/parseTrialOrder';
import { useUpdateProvenance } from './useUpdateProvenance';
import { useReplayContext } from '../../store/hooks/useReplay';
import { syncChannel, syncEmitter } from '../../utils/syncReplay';
import { findNodeAtTime } from '../../utils/findNodeAtTime';

const margin = {
left: 20, top: 0, right: 20, bottom: 0,
Expand Down Expand Up @@ -228,26 +229,10 @@ export function AudioProvenanceVis({
return;
}

let tempNode = provGraph.stimulus.nodes[currentNode];
const foundNodeId = findNodeAtTime(currentNode, playTime, provGraph.stimulus.nodes);

while (true) {
if (playTime < tempNode.createdOn) {
if (!isRootNode(tempNode)) {
const parentNode = tempNode.parent;

tempNode = provGraph.stimulus.nodes[parentNode];
} else break;
} else if (tempNode.children.length > 0) {
const child = tempNode.children[0];

if (playTime > provGraph.stimulus.nodes[child].createdOn) {
tempNode = provGraph.stimulus.nodes[child];
} else break;
} else break;
}

if (tempNode.id !== currentNode) {
_setCurrentNode(tempNode.id);
if (foundNodeId !== currentNode) {
_setCurrentNode(foundNodeId);
}
}, [_setCurrentNode, currentNode, participantId, playTime, taskName, answers]);

Expand Down
27 changes: 6 additions & 21 deletions src/components/audioAnalysis/useUpdateProvenance.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// use effect to control the current provenance node based on the changing playtime.

import { useEffect, useMemo } from 'react';
import { initializeTrrack, isRootNode, Registry } from '@trrack/core';
import { initializeTrrack, Registry } from '@trrack/core';
import { TrrackedProvenance } from '../../store/types';
import { ResponseBlockLocation } from '../../parser/types';
import { findNodeAtTime } from '../../utils/findNodeAtTime';

export function useUpdateProvenance(location: ResponseBlockLocation, playTime: number, provGraph: TrrackedProvenance | undefined, currentNode: string | undefined, setCurrentNode: (node: string | null, _location: ResponseBlockLocation) => void, saveProvenance?: (prov: unknown) => void) {
const trrackInstance = useMemo(() => {
Expand Down Expand Up @@ -38,27 +39,11 @@ export function useUpdateProvenance(location: ResponseBlockLocation, playTime: n
return;
}

let tempNode = provGraph.nodes[currentNode];
const foundNodeId = findNodeAtTime(currentNode, playTime, provGraph.nodes);

while (true) {
if (playTime < tempNode.createdOn) {
if (!isRootNode(tempNode)) {
const parentNode = tempNode.parent;

tempNode = provGraph.nodes[parentNode];
} else break;
} else if (tempNode.children.length > 0) {
const child = tempNode.children[0];

if (playTime > provGraph.nodes[child].createdOn) {
tempNode = provGraph.nodes[child];
} else break;
} else break;
}

if (tempNode.id !== currentNode) {
setCurrentNode(tempNode.id, location);
saveProvenance({ prov: trrackInstance.getState(tempNode), location });
if (foundNodeId !== currentNode) {
setCurrentNode(foundNodeId, location);
saveProvenance({ prov: trrackInstance.getState(provGraph.nodes[foundNodeId]), location });
}
}, [currentNode, playTime, provGraph, location, setCurrentNode, trrackInstance, saveProvenance]);
}
51 changes: 37 additions & 14 deletions src/controllers/VegaController.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {
useCallback, useEffect, useMemo, useState,
useCallback, useEffect, useMemo, useState, type ComponentProps,
} from 'react';
import { Vega, VisualizationSpec, View } from 'react-vega';
import { VegaEmbed } from 'react-vega';
import { initializeTrrack, Registry } from '@trrack/core';
import { VegaProps } from 'react-vega/lib/Vega';
import { View } from 'vega';
import { ValueOf, VegaComponent } from '../parser/types';
import { getJsonAssetByPath } from '../utils/getStaticAsset';
import { ResourceNotFound } from '../ResourceNotFound';
Expand All @@ -13,6 +13,9 @@ import { useCurrentIdentifier } from '../routes/utils';
import { useEvent } from '../store/hooks/useEvent';

type Listeners = { [key: string]: (key: string, value: { responseId: string, response: string | number }) => void };
type VegaEmbedSpec = ComponentProps<typeof VegaEmbed>['spec'];
type SignalListenerHandler = Parameters<View['addSignalListener']>[1];
type VegaSignal = { name: string };

export interface VegaProvState {
event: {
Expand All @@ -21,11 +24,9 @@ export interface VegaProvState {
};
}

const InternalVega = Vega as unknown as React.FC<VegaProps>;

export function VegaController({ currentConfig, provState }: { currentConfig: VegaComponent; provState?: VegaProvState }) {
const storeDispatch = useStoreDispatch();
const [vegaConfig, setVegaConfig] = useState<VisualizationSpec | null>(null);
const [vegaConfig, setVegaConfig] = useState<VegaEmbedSpec | null>(null);
const [loading, setLoading] = useState(true);

const [stimulusStatus, setStimulusStatus] = useState(false);
Expand Down Expand Up @@ -111,28 +112,46 @@ export function VegaController({ currentConfig, provState }: { currentConfig: Ve
});

const signalListeners = useMemo(() => {
const signals = vegaConfig?.config?.signals;
const signals = (vegaConfig && typeof vegaConfig === 'object'
? (vegaConfig as { config?: { signals?: VegaSignal[] } }).config?.signals
: undefined);
if (!signals) return {};

return signals.reduce((listeners, signal) => {
return signals.reduce((listeners: Record<string, SignalListenerHandler>, signal: VegaSignal) => {
if (signal.name === 'revisitAnswer') {
listeners[signal.name] = handleRevisitAnswer;
listeners[signal.name] = handleRevisitAnswer as SignalListenerHandler;
} else {
listeners[signal.name] = handleSignalEvt;
listeners[signal.name] = handleSignalEvt as SignalListenerHandler;
}
return listeners;
}, {} as Listeners);
}, {} as Record<string, SignalListenerHandler>);
}, [handleRevisitAnswer, handleSignalEvt, vegaConfig]);

useEffect(() => {
if (!view) {
return undefined;
}

(Object.entries(signalListeners) as Array<[string, SignalListenerHandler]>).forEach(([name, listener]) => {
view.addSignalListener(name, listener);
});

return () => {
(Object.entries(signalListeners) as Array<[string, SignalListenerHandler]>).forEach(([name, listener]) => {
view.removeSignalListener(name, listener);
});
};
}, [signalListeners, view]);

useEffect(() => {
async function fetchVega() {
setLoading(true);

let config: VisualizationSpec | undefined;
let config: VegaEmbedSpec | undefined;
if ('path' in currentConfig) {
config = await getJsonAssetByPath(currentConfig.path);
} else {
config = currentConfig.config as VisualizationSpec;
config = currentConfig.config as VegaEmbedSpec;
}
if (config !== undefined) {
setVegaConfig(config);
Expand Down Expand Up @@ -162,6 +181,10 @@ export function VegaController({ currentConfig, provState }: { currentConfig: Ve
}

return (
<InternalVega spec={structuredClone(vegaConfig)} signalListeners={signalListeners as never} onNewView={(v) => setView(v)} actions={currentConfig.withActions} />
<VegaEmbed
spec={structuredClone(vegaConfig) as VegaEmbedSpec}
options={{ actions: currentConfig.withActions }}
onEmbed={(result) => setView(result.view)}
/>
);
}
4 changes: 3 additions & 1 deletion src/parser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ export type { ParticipantData, ParticipantDataWithStatus } from '../storage/type
export type { StoredAnswer, ParticipantMetadata } from '../store/types';

export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
/* eslint-disable no-use-before-define */
export interface JsonObject {
[key: string]: JsonValue;
}
export type JsonArray = JsonValue[];
/* eslint-enable no-use-before-define */
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;

/**
* The GlobalConfig is used to generate the list of available studies in the UI.
Expand Down
2 changes: 1 addition & 1 deletion src/public/example-llm-chatbot/assets/LLMInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function LLMInterface({
const trrackInst = initializeTrrack({
registry: reg,
initialState: {
messages: [],
messages: [] as ChatMessage[],
},
});

Expand Down
Loading
Loading