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 (
+
+
+
+ );
+}
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,
+ };
+}