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
12 changes: 12 additions & 0 deletions app/pre-reveal-reflection/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PageHeader } from "@/components/PageHeader";
import { PreRevealReflectionForm } from "@/components/PreRevealReflectionForm";
import { SiteShell } from "@/components/SiteShell";

export default function PreRevealReflectionPage() {
return (
<SiteShell currentStage="pre-reveal">
<PageHeader title="Brief reflection" description="Reflect on your interpretation of the visible results before continuing." />
<PreRevealReflectionForm />
</SiteShell>
);
}
6 changes: 6 additions & 0 deletions components/HiddenRuleReveal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
104 changes: 104 additions & 0 deletions components/PreRevealReflectionForm.tsx
Original file line number Diff line number Diff line change
@@ -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<ResearchSession | null>(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<HTMLFormElement>) {
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 (
<Card>
<form onSubmit={handleSubmit} className="space-y-8">
<HelperNote tone="neutral">Before the next part, please briefly reflect on your interpretation of the visible results.</HelperNote>

<LikertQuestion
name="standByInitialInterpretation"
legend="How willing are you to stand by your current interpretation of why some players ended with lower scores?"
leftLabel="Not willing at all"
rightLabel="Completely willing"
value={standByInitialInterpretation}
onChange={setStandByInitialInterpretation}
/>

<TextQuestion
label="If you want, briefly explain what makes you more or less confident in your interpretation."
value={explanationConfidenceText}
onChange={setExplanationConfidenceText}
placeholder="Optional"
maxLength={500}
/>

{showValidation && !isComplete ? <HelperNote tone="warning">Please select a response from 1–7 before continuing.</HelperNote> : null}

<div className="flex justify-end border-t border-slate-200 pt-6">
<PrimaryButton>Continue</PrimaryButton>
</div>
</form>
</Card>
);
}
23 changes: 17 additions & 6 deletions components/PreRevealSurveyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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") {
Expand Down
3 changes: 3 additions & 0 deletions lib/adminSubmissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down
20 changes: 20 additions & 0 deletions lib/researchExportSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
19 changes: 19 additions & 0 deletions types/research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -173,6 +186,8 @@ export interface ComputedResearchMetrics {
informationSufficiencyRevisionDelta?: number;
changedPrimaryAttribution?: boolean;
revisionMagnitude?: number;
delayedReveal: boolean;
standByInitialInterpretation?: number;
}

export interface ResearchExportAssignedProfile {
Expand All @@ -199,6 +214,8 @@ export interface ResearchExport {
postRevealSurveyStartedAt?: string;
postRevealSurveyCompletedAt?: string;
participantProfile?: ParticipantProfile;
revealTimingCondition?: RevealTimingCondition;
preRevealCommitment?: PreRevealCommitment;
assignedProfile: ResearchExportAssignedProfile;
gameSummary: GameSummary;
gameRounds: GameRoundData[];
Expand Down Expand Up @@ -235,6 +252,8 @@ export interface ResearchSession {
preRevealSurveyRevisedAfterReveal?: PreRevealSurveyAnswers;
revisionAccess?: RevisionAccess;
preRevealRevision?: PreRevealRevision;
revealTimingCondition?: RevealTimingCondition;
preRevealCommitment?: PreRevealCommitment;
postRevealSurvey?: PostRevealSurveyAnswers;
preRevealSurveyStartedAt?: string;
preRevealSurveyCompletedAt?: string;
Expand Down
10 changes: 10 additions & 0 deletions utils/researchMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ export function calculateComputedResearchMetrics({
preRevealSurveyRevisedAfterReveal,
revisionAccess,
preRevealRevision,
revealTimingCondition,
preRevealCommitment,
}: {
game: HiddenCostGameState;
preRevealSurvey: PreRevealSurveyAnswers;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
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, RevisionAccess, StageId } from "@/types/research";
import type { ResearchSession, RevealTimingCondition, RevisionAccess, StageId } from "@/types/research";

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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
};
}