From d89f17e645f2e8983cefc2e5e414b9cdb37e981e Mon Sep 17 00:00:00 2001 From: Spbd1 <148923621+Spbd1@users.noreply.github.com> Date: Mon, 11 May 2026 14:46:10 +0000 Subject: [PATCH] Add post-reveal explanation frame condition --- components/PostRevealSurveyForm.tsx | 32 ++++++++++++------- components/ResultsTable.tsx | 8 ++++- lib/adminSubmissions.ts | 1 + lib/researchExportSchema.ts | 10 ++++++ .../complete-research-export.example.json | 9 ++++-- types/research.ts | 9 ++++++ utils/researchMetrics.ts | 7 +++- utils/session.ts | 20 +++++++++++- 8 files changed, 80 insertions(+), 16 deletions(-) diff --git a/components/PostRevealSurveyForm.tsx b/components/PostRevealSurveyForm.tsx index 132da83..5ab0c8e 100644 --- a/components/PostRevealSurveyForm.tsx +++ b/components/PostRevealSurveyForm.tsx @@ -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 = [ @@ -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["condition"], string>; + const initialAnswers: PostRevealSurveyAnswers = { rememberedPrimaryAttribution: "", rememberedIndividualResponsibility: 0, @@ -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); @@ -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 && @@ -161,7 +171,7 @@ export function PostRevealSurveyForm() { updateAnswer("initialJudgmentAccuracy", value)} /> updateAnswer("perspectiveChange", value)} /> - updateAnswer("openRevision", value)} minLength={10} maxLength={500} /> + updateAnswer("openRevision", value)} minLength={10} maxLength={500} /> {showValidation && !isComplete ? Please answer all closed-ended items and write 10–500 characters in the revision. Your draft has been saved in this browser. : null} diff --git a/components/ResultsTable.tsx b/components/ResultsTable.tsx index 63b10eb..e8e4d4d 100644 --- a/components/ResultsTable.tsx +++ b/components/ResultsTable.tsx @@ -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 ( diff --git a/lib/adminSubmissions.ts b/lib/adminSubmissions.ts index bd3ab3c..0a3f840 100644 --- a/lib/adminSubmissions.ts +++ b/lib/adminSubmissions.ts @@ -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"]), diff --git a/lib/researchExportSchema.ts b/lib/researchExportSchema.ts index 775766b..2a144fe 100644 --- a/lib/researchExportSchema.ts +++ b/lib/researchExportSchema.ts @@ -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({ @@ -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), @@ -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), @@ -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), diff --git a/sample-data/complete-research-export.example.json b/sample-data/complete-research-export.example.json index 18ededf..2738d77 100644 --- a/sample-data/complete-research-export.example.json +++ b/sample-data/complete-research-export.example.json @@ -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", @@ -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", @@ -177,7 +181,8 @@ "attributionCategoryShift": { "pre": "individual choices", "post": "structural constraints" - } + }, + "explanationFrame": "explain-to-self" }, "completeness": { "hasParticipantProfile": true, diff --git a/types/research.ts b/types/research.ts index 44f560a..870bcdc 100644 --- a/types/research.ts +++ b/types/research.ts @@ -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; @@ -166,6 +167,11 @@ export interface PreRevealCommitment { completedAt: string; } +export interface ExplanationFrameCondition { + condition: ExplanationFrameConditionName; + assignedAt: string; +} + export interface ComputedResearchMetrics { responsibilityShift: number; constraintRecognitionShift: number; @@ -197,6 +203,7 @@ export interface ComputedResearchMetrics { rememberedPrimaryAttributionMatchesOriginal: boolean; memoryConfidence: number; memoryDistortionMagnitude: number; + explanationFrame: ExplanationFrameConditionName | null; } export interface ResearchExportAssignedProfile { @@ -225,6 +232,7 @@ export interface ResearchExport { participantProfile?: ParticipantProfile; revealTimingCondition?: RevealTimingCondition; preRevealCommitment?: PreRevealCommitment; + explanationFrameCondition?: ExplanationFrameCondition; assignedProfile: ResearchExportAssignedProfile; gameSummary: GameSummary; gameRounds: GameRoundData[]; @@ -263,6 +271,7 @@ export interface ResearchSession { preRevealRevision?: PreRevealRevision; revealTimingCondition?: RevealTimingCondition; preRevealCommitment?: PreRevealCommitment; + explanationFrameCondition?: ExplanationFrameCondition; postRevealSurvey?: PostRevealSurveyAnswers; preRevealSurveyStartedAt?: string; preRevealSurveyCompletedAt?: string; diff --git a/utils/researchMetrics.ts b/utils/researchMetrics.ts index 842e2b6..c651f9d 100644 --- a/utils/researchMetrics.ts +++ b/utils/researchMetrics.ts @@ -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 = { @@ -106,6 +106,7 @@ export function calculateComputedResearchMetrics({ preRevealRevision, revealTimingCondition, preRevealCommitment, + explanationFrameCondition, }: { game: HiddenCostGameState; preRevealSurvey: PreRevealSurveyAnswers; @@ -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; @@ -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, @@ -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 { @@ -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, diff --git a/utils/session.ts b/utils/session.ts index e7525bc..3c2e763 100644 --- a/utils/session.ts +++ b/utils/session.ts @@ -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"; @@ -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, @@ -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, + }; +}