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
30 changes: 27 additions & 3 deletions components/ResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
isPostRevealSurveyComplete,
isPreRevealSurveyComplete,
} from "@/utils/researchMetrics";
import { getStoredSession, saveStoredSession } from "@/utils/session";
import type { ComputedResearchMetrics, GameSummary, HiddenCostGameState, PostRevealSurveyAnswers, PreRevealSurveyAnswers, ResearchSession } from "@/types/research";
import { assignCostVisibilityCondition, getStoredSession, saveStoredSession } from "@/utils/session";
import type { ComputedResearchMetrics, CostVisibilityConditionName, GameSummary, HiddenCostGameState, PostRevealSurveyAnswers, PreRevealSurveyAnswers, ResearchSession } from "@/types/research";

const fictionalPlayers = [
{ name: "Player 1", score: 164 },
Expand Down Expand Up @@ -52,10 +52,11 @@ export function ResultsTable({ mode = "visible" }: { mode?: ResultsMode }) {
return;
}

const nextSession: ResearchSession = {
const stagedSession: ResearchSession = {
...storedSession,
currentStage: targetStage,
};
const nextSession = mode === "visible" ? assignCostVisibilityCondition(stagedSession) : stagedSession;

saveStoredSession(nextSession);
setSession(nextSession);
Expand Down Expand Up @@ -90,6 +91,8 @@ export function ResultsTable({ mode = "visible" }: { mode?: ResultsMode }) {
At this stage, you can see the score table. Please answer based only on the information currently available.
</p>

{session?.costVisibilityCondition ? <CostVisibilityNote condition={session.costVisibilityCondition.condition} /> : null}

<ResultsRankingTable players={sortedPlayers} />

<div className="flex justify-end border-t border-slate-200 pt-6">
Expand All @@ -99,6 +102,23 @@ export function ResultsTable({ mode = "visible" }: { mode?: ResultsMode }) {
);
}

function CostVisibilityNote({ condition }: { condition: CostVisibilityConditionName }) {
if (condition === "no-cost-info") {
return null;
}

const message =
condition === "partial-cost-hint"
? "The score table may not show all conditions that shaped players' decisions. Some costs or constraints may differ across profiles."
: "Players may have faced different treatment-cost conditions during the game. The score table alone may not fully explain the final outcomes.";

return (
<p className="rounded-2xl border border-amber-200 bg-amber-50 p-5 leading-7 text-amber-950">
{message}
</p>
);
}

function IndividualResults({
session,
game,
Expand All @@ -117,6 +137,7 @@ function IndividualResults({
postRevealSurvey,
preRevealSurveyOriginal: session.preRevealSurveyOriginal,
explanationFrameCondition: session.explanationFrameCondition,
costVisibilityCondition: session.costVisibilityCondition,
});
const interpretations = buildParticipantInterpretation(computedMetrics);

Expand Down Expand Up @@ -232,6 +253,9 @@ function MetricsGrid({ metrics }: { metrics: ComputedResearchMetrics }) {
["Remembered attribution matches original", metrics.rememberedPrimaryAttributionMatchesOriginal ? "Yes" : "No"],
["Memory confidence", metrics.memoryConfidence],
["Memory distortion magnitude", metrics.memoryDistortionMagnitude],
["Cost visibility condition", metrics.costVisibilityCondition ?? "Not assigned"],
["Had any cost hint", metrics.hadAnyCostHint ? "Yes" : "No"],
["Had strong cost hint", metrics.hadStrongCostHint ? "Yes" : "No"],
] as const;

return (
Expand Down
4 changes: 4 additions & 0 deletions lib/adminSubmissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ const CSV_COLUMNS: CsvColumn[] = [
dbColumn("completed_game_rounds", "completedGameRounds"),
payloadColumn("reveal_timing_condition", ["revealTimingCondition", "condition"]),
payloadColumn("explanation_frame_condition", ["explanationFrameCondition", "condition"]),
payloadColumn("cost_visibility_condition", ["costVisibilityCondition", "condition"]),
payloadColumn("stand_by_initial_interpretation", ["preRevealCommitment", "standByInitialInterpretation"]),
payloadColumn("pre_reveal_commitment_text", ["preRevealCommitment", "explanationConfidenceText"]),
payloadColumn("final_financial_score", ["gameSummary", "finalFinancialScore"]),
Expand All @@ -306,6 +307,9 @@ const CSV_COLUMNS: CsvColumn[] = [
payloadColumn("skipped_treatment_choices", ["gameSummary", "skippedTreatmentChoices"]),
payloadColumn("burden", ["computedMetrics", "burden"]),
payloadColumn("care_avoidance", ["computedMetrics", "careAvoidance"]),
payloadColumn("metric_cost_visibility_condition", ["computedMetrics", "costVisibilityCondition"]),
payloadColumn("had_any_cost_hint", ["computedMetrics", "hadAnyCostHint"]),
payloadColumn("had_strong_cost_hint", ["computedMetrics", "hadStrongCostHint"]),
payloadColumn("responsibility_shift", ["computedMetrics", "responsibilityShift"]),
payloadColumn("constraint_recognition_shift", ["computedMetrics", "constraintRecognitionShift"]),
payloadColumn("protest_legitimacy_shift", ["computedMetrics", "protestLegitimacyShift"]),
Expand Down
12 changes: 12 additions & 0 deletions lib/researchExportSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const serverSubmissionStatusSchema = z.enum(["not_enabled", "not_submitted", "su
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"]);
const costVisibilityConditionNameSchema = z.enum(["no-cost-info", "partial-cost-hint", "full-cost-preview"]);

export const revisionAccessSchema = z
.object({
Expand Down Expand Up @@ -55,6 +56,13 @@ export const explanationFrameConditionSchema = z
})
.passthrough();

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

export const participantProfileSchema = z
.object({
ageGroup: z.string().min(1),
Expand Down Expand Up @@ -166,6 +174,9 @@ export const computedMetricsSchema = z
memoryConfidence: likertSchema,
memoryDistortionMagnitude: z.number().nonnegative(),
explanationFrame: explanationFrameConditionNameSchema.nullable(),
costVisibilityCondition: costVisibilityConditionNameSchema.nullable(),
hadAnyCostHint: z.boolean(),
hadStrongCostHint: z.boolean(),
attributionCategoryShift: z
.object({
pre: z.string().min(1),
Expand Down Expand Up @@ -213,6 +224,7 @@ export const researchExportSchema = z
revealTimingCondition: revealTimingConditionSchema.optional(),
preRevealCommitment: preRevealCommitmentSchema.optional(),
explanationFrameCondition: explanationFrameConditionSchema.optional(),
costVisibilityCondition: costVisibilityConditionSchema.optional(),
assignedProfile: assignedProfileSchema,
gameSummary: gameSummarySchema,
gameRounds: z.array(gameRoundSchema).min(1),
Expand Down
11 changes: 11 additions & 0 deletions types/research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export interface PreRevealRevision {

export type RevealTimingConditionName = "immediate-reveal" | "delayed-reveal";
export type ExplanationFrameConditionName = "explain-to-self" | "explain-to-other";
export type CostVisibilityConditionName = "no-cost-info" | "partial-cost-hint" | "full-cost-preview";

export interface RevealTimingCondition {
condition: RevealTimingConditionName;
Expand All @@ -172,6 +173,11 @@ export interface ExplanationFrameCondition {
assignedAt: string;
}

export interface CostVisibilityCondition {
condition: CostVisibilityConditionName;
assignedAt: string;
}

export interface ComputedResearchMetrics {
responsibilityShift: number;
constraintRecognitionShift: number;
Expand Down Expand Up @@ -204,6 +210,9 @@ export interface ComputedResearchMetrics {
memoryConfidence: number;
memoryDistortionMagnitude: number;
explanationFrame: ExplanationFrameConditionName | null;
costVisibilityCondition: CostVisibilityConditionName | null;
hadAnyCostHint: boolean;
hadStrongCostHint: boolean;
}

export interface ResearchExportAssignedProfile {
Expand Down Expand Up @@ -233,6 +242,7 @@ export interface ResearchExport {
revealTimingCondition?: RevealTimingCondition;
preRevealCommitment?: PreRevealCommitment;
explanationFrameCondition?: ExplanationFrameCondition;
costVisibilityCondition?: CostVisibilityCondition;
assignedProfile: ResearchExportAssignedProfile;
gameSummary: GameSummary;
gameRounds: GameRoundData[];
Expand Down Expand Up @@ -272,6 +282,7 @@ export interface ResearchSession {
revealTimingCondition?: RevealTimingCondition;
preRevealCommitment?: PreRevealCommitment;
explanationFrameCondition?: ExplanationFrameCondition;
costVisibilityCondition?: CostVisibilityCondition;
postRevealSurvey?: PostRevealSurveyAnswers;
preRevealSurveyStartedAt?: string;
preRevealSurveyCompletedAt?: string;
Expand Down
10 changes: 9 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-4";
export const RESEARCH_SCHEMA_VERSION = "hidden-cost-game-research-schema-5";
export const RESEARCH_CONSENT_VERSION = "pilot-consent-v1";

const choiceCountKeys: Record<GameChoice, keyof TreatmentChoiceCounts> = {
Expand Down Expand Up @@ -107,6 +107,7 @@ export function calculateComputedResearchMetrics({
revealTimingCondition,
preRevealCommitment,
explanationFrameCondition,
costVisibilityCondition,
}: {
game: HiddenCostGameState;
preRevealSurvey: PreRevealSurveyAnswers;
Expand All @@ -118,6 +119,7 @@ export function calculateComputedResearchMetrics({
revealTimingCondition?: ResearchSession["revealTimingCondition"];
preRevealCommitment?: ResearchSession["preRevealCommitment"];
explanationFrameCondition?: ResearchSession["explanationFrameCondition"];
costVisibilityCondition?: ResearchSession["costVisibilityCondition"];
}): ComputedResearchMetrics {
const summary = calculateGameSummary(game);
const originalPreRevealSurvey = preRevealSurveyOriginal ?? preRevealSurvey;
Expand All @@ -141,6 +143,7 @@ export function calculateComputedResearchMetrics({
informationSufficiencyRevisionDelta,
];
const hasRevisionComparison = revisionDeltas.every((delta): delta is number => typeof delta === "number");
const costVisibilityConditionName = costVisibilityCondition?.condition ?? null;

return {
responsibilityShift: postRevealSurvey.revisedIndividualResponsibility - preRevealSurvey.individualResponsibility,
Expand All @@ -161,6 +164,9 @@ export function calculateComputedResearchMetrics({
memoryConfidence: postRevealSurvey.rememberedConfidence,
memoryDistortionMagnitude: roundMetric(memoryDistortionMagnitude),
explanationFrame: explanationFrameCondition?.condition ?? null,
costVisibilityCondition: costVisibilityConditionName,
hadAnyCostHint: costVisibilityConditionName === "partial-cost-hint" || costVisibilityConditionName === "full-cost-preview",
hadStrongCostHint: costVisibilityConditionName === "full-cost-preview",
attributionCategoryShift: {
pre: preRevealSurvey.primaryAttribution,
post: postRevealSurvey.revisedPrimaryAttribution,
Expand Down Expand Up @@ -259,6 +265,7 @@ export function buildResearchExport(session: ResearchSession, createdAt = new Da
revealTimingCondition: session.revealTimingCondition,
preRevealCommitment: session.preRevealCommitment,
explanationFrameCondition: session.explanationFrameCondition,
costVisibilityCondition: session.costVisibilityCondition,
});

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

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

Expand Down Expand Up @@ -73,6 +73,7 @@ function normalizeStoredSession(value: unknown, currentStage: StageId): Research
revealTimingCondition: candidate.revealTimingCondition,
preRevealCommitment: candidate.preRevealCommitment,
explanationFrameCondition: candidate.explanationFrameCondition,
costVisibilityCondition: candidate.costVisibilityCondition,
postRevealSurvey: candidate.postRevealSurvey,
preRevealSurveyStartedAt: candidate.preRevealSurveyStartedAt,
preRevealSurveyCompletedAt: candidate.preRevealSurveyCompletedAt,
Expand Down Expand Up @@ -128,6 +129,22 @@ export function assignRevealTimingCondition(session: ResearchSession): ResearchS
};
}

export function assignCostVisibilityCondition(session: ResearchSession): ResearchSession {
if (session.costVisibilityCondition) {
return session;
}

const roll = Math.random();
const costVisibilityCondition: CostVisibilityCondition = {
condition: roll < 1 / 3 ? "no-cost-info" : roll < 2 / 3 ? "partial-cost-hint" : "full-cost-preview",
assignedAt: new Date().toISOString(),
};

return {
...session,
costVisibilityCondition,
};
}

export function assignExplanationFrameCondition(session: ResearchSession): ResearchSession {
if (session.explanationFrameCondition) {
Expand Down