From 8bef01119d97c468df81157392d65c5a7bf0e1c6 Mon Sep 17 00:00:00 2001 From: Spbd1 <148923621+Spbd1@users.noreply.github.com> Date: Mon, 11 May 2026 14:25:56 +0000 Subject: [PATCH] Add delayed reveal reflection condition --- app/pre-reveal-reflection/page.tsx | 12 +++ components/HiddenRuleReveal.tsx | 6 ++ components/PreRevealReflectionForm.tsx | 104 +++++++++++++++++++++++++ components/PreRevealSurveyForm.tsx | 23 ++++-- lib/adminSubmissions.ts | 3 + lib/researchExportSchema.ts | 20 +++++ types/research.ts | 19 +++++ utils/researchMetrics.ts | 10 +++ utils/session.ts | 20 ++++- 9 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 app/pre-reveal-reflection/page.tsx create mode 100644 components/PreRevealReflectionForm.tsx diff --git a/app/pre-reveal-reflection/page.tsx b/app/pre-reveal-reflection/page.tsx new file mode 100644 index 0000000..f54a058 --- /dev/null +++ b/app/pre-reveal-reflection/page.tsx @@ -0,0 +1,12 @@ +import { PageHeader } from "@/components/PageHeader"; +import { PreRevealReflectionForm } from "@/components/PreRevealReflectionForm"; +import { SiteShell } from "@/components/SiteShell"; + +export default function PreRevealReflectionPage() { + return ( + + + + + ); +} diff --git a/components/HiddenRuleReveal.tsx b/components/HiddenRuleReveal.tsx index 9fefc9d..0305ff0 100644 --- a/components/HiddenRuleReveal.tsx +++ b/components/HiddenRuleReveal.tsx @@ -22,6 +22,12 @@ export function HiddenRuleReveal() { return; } + if (storedSession.revealTimingCondition?.condition === "delayed-reveal" && !storedSession.preRevealCommitment && !storedSession.revealViewedAt) { + saveStoredSession({ ...storedSession, currentStage: "pre-reveal" }); + router.replace("/pre-reveal-reflection"); + return; + } + const nextSession: ResearchSession = { ...storedSession, currentStage: "reveal", diff --git a/components/PreRevealReflectionForm.tsx b/components/PreRevealReflectionForm.tsx new file mode 100644 index 0000000..d9a19f7 --- /dev/null +++ b/components/PreRevealReflectionForm.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { FormEvent } from "react"; +import { useRouter } from "next/navigation"; +import { Card } from "@/components/Card"; +import { HelperNote, LikertQuestion, PrimaryButton, TextQuestion } from "@/components/FormControls"; +import { isPreRevealSurveyComplete } from "@/utils/researchMetrics"; +import { getStoredSession, saveStoredSession } from "@/utils/session"; +import type { ResearchSession } from "@/types/research"; + +export function PreRevealReflectionForm() { + const router = useRouter(); + const [session, setSession] = useState(null); + const [standByInitialInterpretation, setStandByInitialInterpretation] = useState(0); + const [explanationConfidenceText, setExplanationConfidenceText] = useState(""); + const [showValidation, setShowValidation] = useState(false); + + useEffect(() => { + const storedSession = getStoredSession("pre-reveal"); + + if (!storedSession.game?.completedAt || !isPreRevealSurveyComplete(storedSession.preRevealSurvey)) { + const fallbackSession: ResearchSession = { + ...storedSession, + currentStage: storedSession.game?.completedAt ? "pre-reveal" : "game", + }; + saveStoredSession(fallbackSession); + router.replace(storedSession.game?.completedAt ? "/pre-reveal-survey" : "/game"); + return; + } + + if (storedSession.revealViewedAt || storedSession.revealTimingCondition?.condition !== "delayed-reveal") { + saveStoredSession({ ...storedSession, currentStage: "reveal" }); + router.replace("/hidden-rule-reveal"); + return; + } + + const nextSession: ResearchSession = { + ...storedSession, + currentStage: "pre-reveal", + }; + + saveStoredSession(nextSession); + setSession(nextSession); + setStandByInitialInterpretation(nextSession.preRevealCommitment?.standByInitialInterpretation ?? 0); + setExplanationConfidenceText(nextSession.preRevealCommitment?.explanationConfidenceText ?? ""); + }, [router]); + + const trimmedText = explanationConfidenceText.trim(); + const isComplete = standByInitialInterpretation > 0 && trimmedText.length <= 500; + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + setShowValidation(true); + + if (!session || !isComplete) { + return; + } + + const updatedSession: ResearchSession = { + ...session, + currentStage: "reveal", + preRevealCommitment: { + standByInitialInterpretation, + ...(trimmedText ? { explanationConfidenceText: trimmedText } : {}), + completedAt: new Date().toISOString(), + }, + }; + + saveStoredSession(updatedSession); + router.push("/hidden-rule-reveal"); + } + + return ( + +
+ Before the next part, please briefly reflect on your interpretation of the visible results. + + + + + + {showValidation && !isComplete ? Please select a response from 1–7 before continuing. : null} + +
+ Continue +
+ +
+ ); +} diff --git a/components/PreRevealSurveyForm.tsx b/components/PreRevealSurveyForm.tsx index d0b7eb1..3c750cf 100644 --- a/components/PreRevealSurveyForm.tsx +++ b/components/PreRevealSurveyForm.tsx @@ -6,7 +6,7 @@ import { useRouter } from "next/navigation"; import { ButtonLink } from "@/components/ButtonLink"; import { Card } from "@/components/Card"; import { HelperNote, LikertQuestion, PrimaryButton, SingleChoiceQuestion, TextQuestion } from "@/components/FormControls"; -import { assignPreRevealRevisionAccess, getStoredSession, saveStoredSession } from "@/utils/session"; +import { assignPreRevealRevisionAccess, assignRevealTimingCondition, getStoredSession, saveStoredSession } from "@/utils/session"; import type { PreRevealSurveyAnswers, ResearchSession } from "@/types/research"; const primaryAttributionOptions = [ @@ -157,16 +157,27 @@ export function PreRevealSurveyForm() { blockedAt: session.preRevealRevision?.blockedAt, }, } - : { + : assignRevealTimingCondition({ ...session, - currentStage: "reveal", preRevealSurveyCompletedAt: now, preRevealSurvey: submittedAnswers, preRevealSurveyOriginal: session.preRevealSurveyOriginal ?? submittedAnswers, - }; + }); - saveStoredSession(updatedSession); - router.push(revisionMode === "revision-unlocked" ? "/post-reveal-survey" : "/hidden-rule-reveal"); + const nextPath = + revisionMode === "revision-unlocked" + ? "/post-reveal-survey" + : updatedSession.revealTimingCondition?.condition === "delayed-reveal" + ? "/pre-reveal-reflection" + : "/hidden-rule-reveal"; + const nextStage = revisionMode === "revision-unlocked" ? "post-reveal" : updatedSession.revealTimingCondition?.condition === "delayed-reveal" ? "pre-reveal" : "reveal"; + const stageSession: ResearchSession = { + ...updatedSession, + currentStage: nextStage, + }; + + saveStoredSession(stageSession); + router.push(nextPath); } if (revisionMode === "revision-locked") { diff --git a/lib/adminSubmissions.ts b/lib/adminSubmissions.ts index 7926237..b8164d6 100644 --- a/lib/adminSubmissions.ts +++ b/lib/adminSubmissions.ts @@ -293,6 +293,9 @@ const CSV_COLUMNS: CsvColumn[] = [ dbColumn("assigned_displayed_profile", "assignedDisplayedProfile"), dbColumn("assigned_hidden_profile", "assignedHiddenProfile"), dbColumn("completed_game_rounds", "completedGameRounds"), + payloadColumn("reveal_timing_condition", ["revealTimingCondition", "condition"]), + payloadColumn("stand_by_initial_interpretation", ["preRevealCommitment", "standByInitialInterpretation"]), + payloadColumn("pre_reveal_commitment_text", ["preRevealCommitment", "explanationConfidenceText"]), payloadColumn("final_financial_score", ["gameSummary", "finalFinancialScore"]), payloadColumn("final_health_score", ["gameSummary", "finalHealthScore"]), payloadColumn("total_treatment_cost_paid", ["gameSummary", "totalTreatmentCostPaid"]), diff --git a/lib/researchExportSchema.ts b/lib/researchExportSchema.ts index cd399b7..cd026ac 100644 --- a/lib/researchExportSchema.ts +++ b/lib/researchExportSchema.ts @@ -10,6 +10,7 @@ const gameChoiceSchema = z.enum(["full-treatment", "partial-treatment", "skip-tr 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"]); export const revisionAccessSchema = z .object({ @@ -31,6 +32,21 @@ export const preRevealRevisionSchema = z }) .passthrough(); +export const revealTimingConditionSchema = z + .object({ + condition: revealTimingConditionNameSchema, + assignedAt: isoDateStringSchema, + }) + .passthrough(); + +export const preRevealCommitmentSchema = z + .object({ + standByInitialInterpretation: likertSchema, + explanationConfidenceText: z.string().trim().max(500).optional(), + completedAt: isoDateStringSchema, + }) + .passthrough(); + export const participantProfileSchema = z .object({ ageGroup: z.string().min(1), @@ -130,6 +146,8 @@ export const computedMetricsSchema = z perspectiveChange: z.number(), burden: z.number(), careAvoidance: z.number(), + delayedReveal: z.boolean(), + standByInitialInterpretation: likertSchema.optional(), attributionCategoryShift: z .object({ pre: z.string().min(1), @@ -174,6 +192,8 @@ export const researchExportSchema = z serverSubmittedAt: isoDateStringSchema.optional(), createdAt: isoDateStringSchema.optional(), sessionCreatedAt: isoDateStringSchema.optional(), + revealTimingCondition: revealTimingConditionSchema.optional(), + preRevealCommitment: preRevealCommitmentSchema.optional(), assignedProfile: assignedProfileSchema, gameSummary: gameSummarySchema, gameRounds: z.array(gameRoundSchema).min(1), diff --git a/types/research.ts b/types/research.ts index 5d676bd..e715e4f 100644 --- a/types/research.ts +++ b/types/research.ts @@ -149,6 +149,19 @@ export interface PreRevealRevision { blockedAt?: string; } +export type RevealTimingConditionName = "immediate-reveal" | "delayed-reveal"; + +export interface RevealTimingCondition { + condition: RevealTimingConditionName; + assignedAt: string; +} + +export interface PreRevealCommitment { + standByInitialInterpretation: number; + explanationConfidenceText?: string; + completedAt: string; +} + export interface ComputedResearchMetrics { responsibilityShift: number; constraintRecognitionShift: number; @@ -173,6 +186,8 @@ export interface ComputedResearchMetrics { informationSufficiencyRevisionDelta?: number; changedPrimaryAttribution?: boolean; revisionMagnitude?: number; + delayedReveal: boolean; + standByInitialInterpretation?: number; } export interface ResearchExportAssignedProfile { @@ -199,6 +214,8 @@ export interface ResearchExport { postRevealSurveyStartedAt?: string; postRevealSurveyCompletedAt?: string; participantProfile?: ParticipantProfile; + revealTimingCondition?: RevealTimingCondition; + preRevealCommitment?: PreRevealCommitment; assignedProfile: ResearchExportAssignedProfile; gameSummary: GameSummary; gameRounds: GameRoundData[]; @@ -235,6 +252,8 @@ export interface ResearchSession { preRevealSurveyRevisedAfterReveal?: PreRevealSurveyAnswers; revisionAccess?: RevisionAccess; preRevealRevision?: PreRevealRevision; + revealTimingCondition?: RevealTimingCondition; + preRevealCommitment?: PreRevealCommitment; postRevealSurvey?: PostRevealSurveyAnswers; preRevealSurveyStartedAt?: string; preRevealSurveyCompletedAt?: string; diff --git a/utils/researchMetrics.ts b/utils/researchMetrics.ts index fd45a8f..155824e 100644 --- a/utils/researchMetrics.ts +++ b/utils/researchMetrics.ts @@ -100,6 +100,8 @@ export function calculateComputedResearchMetrics({ preRevealSurveyRevisedAfterReveal, revisionAccess, preRevealRevision, + revealTimingCondition, + preRevealCommitment, }: { game: HiddenCostGameState; preRevealSurvey: PreRevealSurveyAnswers; @@ -108,6 +110,8 @@ export function calculateComputedResearchMetrics({ preRevealSurveyRevisedAfterReveal?: PreRevealSurveyAnswers; revisionAccess?: ResearchSession["revisionAccess"]; preRevealRevision?: ResearchSession["preRevealRevision"]; + revealTimingCondition?: ResearchSession["revealTimingCondition"]; + preRevealCommitment?: ResearchSession["preRevealCommitment"]; }): ComputedResearchMetrics { const summary = calculateGameSummary(game); const responsibilityRevisionDelta = preRevealSurveyRevisedAfterReveal && preRevealSurveyOriginal ? preRevealSurveyRevisedAfterReveal.individualResponsibility - preRevealSurveyOriginal.individualResponsibility : undefined; @@ -139,6 +143,8 @@ export function calculateComputedResearchMetrics({ perspectiveChange: postRevealSurvey.perspectiveChange, burden: roundMetric(summary.totalTreatmentCostPaid / Math.max(summary.totalIncome, 1)), careAvoidance: summary.skippedTreatmentChoices + 0.5 * summary.partialTreatmentChoices, + delayedReveal: revealTimingCondition?.condition === "delayed-reveal", + ...(preRevealCommitment ? { standByInitialInterpretation: preRevealCommitment.standByInitialInterpretation } : {}), attributionCategoryShift: { pre: preRevealSurvey.primaryAttribution, post: postRevealSurvey.revisedPrimaryAttribution, @@ -234,6 +240,8 @@ export function buildResearchExport(session: ResearchSession, createdAt = new Da preRevealSurveyRevisedAfterReveal: session.preRevealSurveyRevisedAfterReveal, revisionAccess: session.revisionAccess, preRevealRevision: session.preRevealRevision, + revealTimingCondition: session.revealTimingCondition, + preRevealCommitment: session.preRevealCommitment, }); return { @@ -252,6 +260,8 @@ export function buildResearchExport(session: ResearchSession, createdAt = new Da postRevealSurveyStartedAt: session.postRevealSurveyStartedAt, postRevealSurveyCompletedAt: session.postRevealSurveyCompletedAt, participantProfile: session.participantProfile, + ...(session.revealTimingCondition ? { revealTimingCondition: session.revealTimingCondition } : {}), + ...(session.preRevealCommitment ? { preRevealCommitment: session.preRevealCommitment } : {}), assignedProfile: { displayedProfile: session.game.displayedProfile, hiddenProfile: session.game.hiddenProfile, diff --git a/utils/session.ts b/utils/session.ts index 6af2924..e7525bc 100644 --- a/utils/session.ts +++ b/utils/session.ts @@ -1,4 +1,4 @@ -import type { ResearchSession, RevisionAccess, StageId } from "@/types/research"; +import type { ResearchSession, RevealTimingCondition, RevisionAccess, StageId } from "@/types/research"; export const STORAGE_KEY = "hidden-cost-game-session"; @@ -70,6 +70,8 @@ function normalizeStoredSession(value: unknown, currentStage: StageId): Research preRevealSurveyRevisedAfterReveal: candidate.preRevealSurveyRevisedAfterReveal, revisionAccess: candidate.revisionAccess, preRevealRevision: candidate.preRevealRevision, + revealTimingCondition: candidate.revealTimingCondition, + preRevealCommitment: candidate.preRevealCommitment, postRevealSurvey: candidate.postRevealSurvey, preRevealSurveyStartedAt: candidate.preRevealSurveyStartedAt, preRevealSurveyCompletedAt: candidate.preRevealSurveyCompletedAt, @@ -108,3 +110,19 @@ export function assignPreRevealRevisionAccess(session: ResearchSession): Researc revisionAccess, }; } + +export function assignRevealTimingCondition(session: ResearchSession): ResearchSession { + if (session.revealTimingCondition) { + return session; + } + + const revealTimingCondition: RevealTimingCondition = { + condition: Math.random() < 0.5 ? "immediate-reveal" : "delayed-reveal", + assignedAt: new Date().toISOString(), + }; + + return { + ...session, + revealTimingCondition, + }; +}