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
32 changes: 21 additions & 11 deletions components/PostRevealSurveyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { FormEvent } from "react";
import { useRouter } from "next/navigation";
import { Card } from "@/components/Card";
import { HelperNote, LikertQuestion, PrimaryButton, SingleChoiceQuestion, TextQuestion } from "@/components/FormControls";
import { getStoredSession, saveStoredSession } from "@/utils/session";
import { assignExplanationFrameCondition, getStoredSession, saveStoredSession } from "@/utils/session";
import type { PostRevealSurveyAnswers, ResearchSession } from "@/types/research";

const rememberedPrimaryAttributionOptions = [
Expand All @@ -25,6 +25,11 @@ const revisedPrimaryAttributionOptions = [
"I am still unsure",
];

const explanationFrameQuestionText = {
"explain-to-self": "In one or two sentences, explain to yourself how the hidden cost rule changed, confirmed, or complicated your interpretation.",
"explain-to-other": "Imagine explaining this result to another participant who only saw the score table. In one or two sentences, explain how the hidden cost rule changes, confirms, or complicates the interpretation.",
} satisfies Record<NonNullable<ResearchSession["explanationFrameCondition"]>["condition"], string>;

const initialAnswers: PostRevealSurveyAnswers = {
rememberedPrimaryAttribution: "",
rememberedIndividualResponsibility: 0,
Expand Down Expand Up @@ -61,10 +66,11 @@ export function PostRevealSurveyForm() {
}

const now = new Date().toISOString();
const assignedSession = assignExplanationFrameCondition(storedSession);
const nextSession: ResearchSession = {
...storedSession,
...assignedSession,
currentStage: "post-reveal",
postRevealSurveyStartedAt: storedSession.postRevealSurveyStartedAt ?? now,
postRevealSurveyStartedAt: assignedSession.postRevealSurveyStartedAt ?? now,
};

saveStoredSession(nextSession);
Expand All @@ -73,15 +79,19 @@ export function PostRevealSurveyForm() {
}, [router]);

useEffect(() => {
if (!session) {
return;
}

const updatedSession = { ...session, postRevealSurvey: answers };
saveStoredSession(updatedSession);
setSession(updatedSession);
setSession((currentSession) => {
if (!currentSession) {
return currentSession;
}

const updatedSession = { ...currentSession, postRevealSurvey: answers };
saveStoredSession(updatedSession);
return updatedSession;
});
}, [answers]);

const explanationFrame = session?.explanationFrameCondition?.condition ?? "explain-to-self";
const openRevisionQuestion = explanationFrameQuestionText[explanationFrame];
const openLength = answers.openRevision.trim().length;
const isComplete =
answers.rememberedPrimaryAttribution.length > 0 &&
Expand Down Expand Up @@ -161,7 +171,7 @@ export function PostRevealSurveyForm() {
<LikertQuestion name="initialJudgmentAccuracy" legend="11. Looking back, how accurate does your initial interpretation seem now?" leftLabel="Not accurate at all" rightLabel="Very accurate" value={answers.initialJudgmentAccuracy} onChange={(value) => updateAnswer("initialJudgmentAccuracy", value)} />
<LikertQuestion name="perspectiveChange" legend="12. How much did the reveal change how you view the lower-scoring players?" leftLabel="Did not change at all" rightLabel="Changed a lot" value={answers.perspectiveChange} onChange={(value) => updateAnswer("perspectiveChange", value)} />

<TextQuestion label="13. In one or two sentences, describe how the reveal changed, confirmed, or complicated your interpretation." value={answers.openRevision} onChange={(value) => updateAnswer("openRevision", value)} minLength={10} maxLength={500} />
<TextQuestion label={`13. ${openRevisionQuestion}`} value={answers.openRevision} onChange={(value) => updateAnswer("openRevision", value)} minLength={10} maxLength={500} />

{showValidation && !isComplete ? <HelperNote tone="warning">Please answer all closed-ended items and write 10–500 characters in the revision. Your draft has been saved in this browser.</HelperNote> : null}

Expand Down
8 changes: 7 additions & 1 deletion components/ResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@ function IndividualResults({
postRevealSurvey: PostRevealSurveyAnswers;
}) {
const gameSummary = calculateGameSummary(game);
const computedMetrics = calculateComputedResearchMetrics({ game, preRevealSurvey, postRevealSurvey, preRevealSurveyOriginal: session.preRevealSurveyOriginal });
const computedMetrics = calculateComputedResearchMetrics({
game,
preRevealSurvey,
postRevealSurvey,
preRevealSurveyOriginal: session.preRevealSurveyOriginal,
explanationFrameCondition: session.explanationFrameCondition,
});
const interpretations = buildParticipantInterpretation(computedMetrics);

return (
Expand Down
1 change: 1 addition & 0 deletions lib/adminSubmissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ const CSV_COLUMNS: CsvColumn[] = [
dbColumn("assigned_hidden_profile", "assignedHiddenProfile"),
dbColumn("completed_game_rounds", "completedGameRounds"),
payloadColumn("reveal_timing_condition", ["revealTimingCondition", "condition"]),
payloadColumn("explanation_frame_condition", ["explanationFrameCondition", "condition"]),
payloadColumn("stand_by_initial_interpretation", ["preRevealCommitment", "standByInitialInterpretation"]),
payloadColumn("pre_reveal_commitment_text", ["preRevealCommitment", "explanationConfidenceText"]),
payloadColumn("final_financial_score", ["gameSummary", "finalFinancialScore"]),
Expand Down
10 changes: 10 additions & 0 deletions lib/researchExportSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const medicalRiskLevelSchema = z.enum(["low", "medium", "high"]);
const serverSubmissionStatusSchema = z.enum(["not_enabled", "not_submitted", "submitting", "submitted", "failed"]);
const revisionAccessConditionSchema = z.enum(["revision-unlocked", "revision-locked"]);
const revealTimingConditionNameSchema = z.enum(["immediate-reveal", "delayed-reveal"]);
const explanationFrameConditionNameSchema = z.enum(["explain-to-self", "explain-to-other"]);

export const revisionAccessSchema = z
.object({
Expand Down Expand Up @@ -47,6 +48,13 @@ export const preRevealCommitmentSchema = z
})
.passthrough();

export const explanationFrameConditionSchema = z
.object({
condition: explanationFrameConditionNameSchema,
assignedAt: isoDateStringSchema,
})
.passthrough();

export const participantProfileSchema = z
.object({
ageGroup: z.string().min(1),
Expand Down Expand Up @@ -157,6 +165,7 @@ export const computedMetricsSchema = z
rememberedPrimaryAttributionMatchesOriginal: z.boolean(),
memoryConfidence: likertSchema,
memoryDistortionMagnitude: z.number().nonnegative(),
explanationFrame: explanationFrameConditionNameSchema.nullable(),
attributionCategoryShift: z
.object({
pre: z.string().min(1),
Expand Down Expand Up @@ -203,6 +212,7 @@ export const researchExportSchema = z
sessionCreatedAt: isoDateStringSchema.optional(),
revealTimingCondition: revealTimingConditionSchema.optional(),
preRevealCommitment: preRevealCommitmentSchema.optional(),
explanationFrameCondition: explanationFrameConditionSchema.optional(),
assignedProfile: assignedProfileSchema,
gameSummary: gameSummarySchema,
gameRounds: z.array(gameRoundSchema).min(1),
Expand Down
9 changes: 7 additions & 2 deletions sample-data/complete-research-export.example.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"exportVersion": "prototype-1.2",
"schemaVersion": "hidden-cost-game-research-schema-3",
"schemaVersion": "hidden-cost-game-research-schema-4",
"sessionId": "hcg-example-session",
"consentVersion": "pilot-consent-v1",
"serverSubmissionStatus": "not_submitted",
Expand All @@ -23,6 +23,10 @@
"priorExposureToUnequalSystems": "Example fake response",
"policyPreferenceBaseline": "Prefer not to answer"
},
"explanationFrameCondition": {
"condition": "explain-to-self",
"assignedAt": "2026-05-08T00:14:00.000Z"
},
"assignedProfile": {
"displayedProfile": "Profile B",
"hiddenProfile": "Low coverage",
Expand Down Expand Up @@ -177,7 +181,8 @@
"attributionCategoryShift": {
"pre": "individual choices",
"post": "structural constraints"
}
},
"explanationFrame": "explain-to-self"
},
"completeness": {
"hasParticipantProfile": true,
Expand Down
9 changes: 9 additions & 0 deletions types/research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export interface PreRevealRevision {
}

export type RevealTimingConditionName = "immediate-reveal" | "delayed-reveal";
export type ExplanationFrameConditionName = "explain-to-self" | "explain-to-other";

export interface RevealTimingCondition {
condition: RevealTimingConditionName;
Expand All @@ -166,6 +167,11 @@ export interface PreRevealCommitment {
completedAt: string;
}

export interface ExplanationFrameCondition {
condition: ExplanationFrameConditionName;
assignedAt: string;
}

export interface ComputedResearchMetrics {
responsibilityShift: number;
constraintRecognitionShift: number;
Expand Down Expand Up @@ -197,6 +203,7 @@ export interface ComputedResearchMetrics {
rememberedPrimaryAttributionMatchesOriginal: boolean;
memoryConfidence: number;
memoryDistortionMagnitude: number;
explanationFrame: ExplanationFrameConditionName | null;
}

export interface ResearchExportAssignedProfile {
Expand Down Expand Up @@ -225,6 +232,7 @@ export interface ResearchExport {
participantProfile?: ParticipantProfile;
revealTimingCondition?: RevealTimingCondition;
preRevealCommitment?: PreRevealCommitment;
explanationFrameCondition?: ExplanationFrameCondition;
assignedProfile: ResearchExportAssignedProfile;
gameSummary: GameSummary;
gameRounds: GameRoundData[];
Expand Down Expand Up @@ -263,6 +271,7 @@ export interface ResearchSession {
preRevealRevision?: PreRevealRevision;
revealTimingCondition?: RevealTimingCondition;
preRevealCommitment?: PreRevealCommitment;
explanationFrameCondition?: ExplanationFrameCondition;
postRevealSurvey?: PostRevealSurveyAnswers;
preRevealSurveyStartedAt?: string;
preRevealSurveyCompletedAt?: string;
Expand Down
7 changes: 6 additions & 1 deletion utils/researchMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
} from "@/types/research";

export const RESEARCH_EXPORT_VERSION = "prototype-1.2";
export const RESEARCH_SCHEMA_VERSION = "hidden-cost-game-research-schema-3";
export const RESEARCH_SCHEMA_VERSION = "hidden-cost-game-research-schema-4";
export const RESEARCH_CONSENT_VERSION = "pilot-consent-v1";

const choiceCountKeys: Record<GameChoice, keyof TreatmentChoiceCounts> = {
Expand Down Expand Up @@ -106,6 +106,7 @@ export function calculateComputedResearchMetrics({
preRevealRevision,
revealTimingCondition,
preRevealCommitment,
explanationFrameCondition,
}: {
game: HiddenCostGameState;
preRevealSurvey: PreRevealSurveyAnswers;
Expand All @@ -116,6 +117,7 @@ export function calculateComputedResearchMetrics({
preRevealRevision?: ResearchSession["preRevealRevision"];
revealTimingCondition?: ResearchSession["revealTimingCondition"];
preRevealCommitment?: ResearchSession["preRevealCommitment"];
explanationFrameCondition?: ResearchSession["explanationFrameCondition"];
}): ComputedResearchMetrics {
const summary = calculateGameSummary(game);
const originalPreRevealSurvey = preRevealSurveyOriginal ?? preRevealSurvey;
Expand Down Expand Up @@ -158,6 +160,7 @@ export function calculateComputedResearchMetrics({
rememberedPrimaryAttributionMatchesOriginal: postRevealSurvey.rememberedPrimaryAttribution === originalPreRevealSurvey.primaryAttribution,
memoryConfidence: postRevealSurvey.rememberedConfidence,
memoryDistortionMagnitude: roundMetric(memoryDistortionMagnitude),
explanationFrame: explanationFrameCondition?.condition ?? null,
attributionCategoryShift: {
pre: preRevealSurvey.primaryAttribution,
post: postRevealSurvey.revisedPrimaryAttribution,
Expand Down Expand Up @@ -255,6 +258,7 @@ export function buildResearchExport(session: ResearchSession, createdAt = new Da
preRevealRevision: session.preRevealRevision,
revealTimingCondition: session.revealTimingCondition,
preRevealCommitment: session.preRevealCommitment,
explanationFrameCondition: session.explanationFrameCondition,
});

return {
Expand All @@ -275,6 +279,7 @@ export function buildResearchExport(session: ResearchSession, createdAt = new Da
participantProfile: session.participantProfile,
...(session.revealTimingCondition ? { revealTimingCondition: session.revealTimingCondition } : {}),
...(session.preRevealCommitment ? { preRevealCommitment: session.preRevealCommitment } : {}),
...(session.explanationFrameCondition ? { explanationFrameCondition: session.explanationFrameCondition } : {}),
assignedProfile: {
displayedProfile: session.game.displayedProfile,
hiddenProfile: session.game.hiddenProfile,
Expand Down
20 changes: 19 additions & 1 deletion utils/session.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ResearchSession, RevealTimingCondition, RevisionAccess, StageId } from "@/types/research";
import type { ExplanationFrameCondition, ResearchSession, RevealTimingCondition, RevisionAccess, StageId } from "@/types/research";

export const STORAGE_KEY = "hidden-cost-game-session";

Expand Down Expand Up @@ -72,6 +72,7 @@ function normalizeStoredSession(value: unknown, currentStage: StageId): Research
preRevealRevision: candidate.preRevealRevision,
revealTimingCondition: candidate.revealTimingCondition,
preRevealCommitment: candidate.preRevealCommitment,
explanationFrameCondition: candidate.explanationFrameCondition,
postRevealSurvey: candidate.postRevealSurvey,
preRevealSurveyStartedAt: candidate.preRevealSurveyStartedAt,
preRevealSurveyCompletedAt: candidate.preRevealSurveyCompletedAt,
Expand Down Expand Up @@ -126,3 +127,20 @@ export function assignRevealTimingCondition(session: ResearchSession): ResearchS
revealTimingCondition,
};
}


export function assignExplanationFrameCondition(session: ResearchSession): ResearchSession {
if (session.explanationFrameCondition) {
return session;
}

const explanationFrameCondition: ExplanationFrameCondition = {
condition: Math.random() < 0.5 ? "explain-to-self" : "explain-to-other",
assignedAt: new Date().toISOString(),
};

return {
...session,
explanationFrameCondition,
};
}