""" Trust in Human-AI Teams in B2B Supplier Selection oTree Experiment — __init__.py (v3) Changes from v2: - Information asymmetry: PA sees PA criteria only, SA sees CSR criteria only (Briefing) - 30 % AI error: fixed to Supplier E (not random) — consistent across sessions - Round-specific explanations (ROUND_EXPLANATIONS): High Transparency rationale varies per round - Meaning-centred feedback texts: number-heavy → descriptive language - Separate feedback tables for AI=A sessions and AI=E sessions - SA no longer sees AI recommendation: sequential AI→PA→SA structure enforced - Monetary payoff: €0.20 per round where team chose optimal Supplier A """ import random from otree.api import ( BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Page, WaitPage, models, widgets, Currency, ) # ───────────────────────────────────────────── # 1. CONSTANTS & SUPPLIER DATA # ───────────────────────────────────────────── SUPPLIER_DATA = { 'A': { # optimal supplier 'pa': {'cost': 0.800, 'delivery': 0.714, 'innovation': 0.800}, 'sa': {'human_rights': 0.750, 'local': 0.600, 'carbon': 1.000}, }, 'B': { 'pa': {'cost': 0.400, 'delivery': 0.429, 'innovation': 0.400}, 'sa': {'human_rights': 0.500, 'local': 0.400, 'carbon': 0.429}, }, 'C': { 'pa': {'cost': 0.000, 'delivery': 0.857, 'innovation': 1.000}, 'sa': {'human_rights': 1.000, 'local': 0.800, 'carbon': 0.714}, }, 'D': { 'pa': {'cost': 0.600, 'delivery': 0.000, 'innovation': 0.200}, 'sa': {'human_rights': 0.000, 'local': 0.000, 'carbon': 0.179}, }, 'E': { # AI error supplier (30 % sessions) 'pa': {'cost': 1.000, 'delivery': 0.286, 'innovation': 0.000}, 'sa': {'human_rights': 0.250, 'local': 0.200, 'carbon': 0.000}, }, 'F': { 'pa': {'cost': 0.200, 'delivery': 1.000, 'innovation': 0.600}, 'sa': {'human_rights': 0.750, 'local': 1.000, 'carbon': 0.750}, }, } AI_WEIGHTS_PA = {'cost': 0.30, 'delivery': 0.40, 'innovation': 0.30} AI_WEIGHTS_SA = {'human_rights': 0.333, 'local': 0.333, 'carbon': 0.334} OPTIMAL_SUPPLIER = 'A' ERROR_SUPPLIER = 'E' # fixed non-optimal recommendation (30 % sessions) AI_ACCURACY = 0.70 SUPPLIERS = ['A', 'B', 'C', 'D', 'E', 'F'] BONUS_PER_OPTIMAL_ROUND = 0.20 # € per round where team chose Supplier A # ───────────────────────────────────────────── # 2. ROUND-SPECIFIC EXPLANATIONS (High Transparency, PA page) # ───────────────────────────────────────────── ROUND_EXPLANATIONS = { # 70 % sessions — AI correctly recommends A 'A': { 1: ("This round, the AI weighted delivery reliability as the most critical factor. " "Supplier A's delivery performance consistently outperforms most alternatives " "in the pool, making it the preferred choice."), 2: ("Cost efficiency proved decisive this cycle. Supplier A offers strong price " "competitiveness — second-best in the pool — while maintaining solid delivery " "and innovation performance."), 3: ("Innovation capability was central to this round's evaluation. Supplier A's " "consistent R&D investment places it among the top performers, supporting " "long-term competitiveness."), 4: ("Environmental screening played a key role this cycle. Supplier A carries the " "lowest carbon footprint in the pool, making it a standout on sustainability " "alongside its purchasing strengths."), 5: ("Across all five rounds, the AI's holistic analysis confirms that Supplier A " "provides the strongest balanced profile — competitive in cost, reliable in " "delivery, capable in innovation, and responsible in sustainability."), }, # 30 % sessions — AI incorrectly recommends E (cost-overweighting error) 'E': { 1: ("This round, the AI prioritised cost efficiency above all other purchasing " "criteria. Supplier E offers the lowest price point in the pool, giving it " "a clear edge on this dimension."), 2: ("Cost remained the dominant signal this cycle. Supplier E's pricing structure " "provides a consistent cost advantage that the AI's model weighted most heavily."), 3: ("Short-term procurement cost savings continued to drive the analysis. " "Supplier E maintains the strongest price position relative to all alternatives."), 4: ("Unit cost efficiency remained the primary criterion this cycle. Supplier E's " "price advantage is consistent across rounds, keeping it the AI's preferred " "choice on this dimension."), 5: ("Across five rounds, cost efficiency has been the central driver of this " "recommendation. Supplier E's pricing profile remains the strongest in the " "pool on this criterion."), }, } # ───────────────────────────────────────────── # 3. FEEDBACK LOOKUP TABLES # Separate tables for AI=A sessions and AI=E sessions. # Keyed by team's final choice (sa_choice). # Meaning-centred language — no raw score comparisons. # ───────────────────────────────────────────── # ── When AI recommended A (70 % sessions) ──── PA_FEEDBACK_AI_A = { 'A': { # team aligned with AI 'high': "The team's purchasing choice aligns with the AI recommendation. " "The AI maintains: Supplier A.", 'low': "The AI maintains its recommendation: Supplier A.", }, 'B': { 'high': ("Supplier B falls short on all three purchasing dimensions. " "Compared to Supplier A, it is more expensive, slower to deliver, " "and offers weaker innovation capacity. The AI maintains: Supplier A."), 'low': "Based on updated analysis, the AI maintains its recommendation: Supplier A.", }, 'C': { 'high': ("Supplier C impresses on delivery speed and innovation potential, " "but its significantly higher cost makes it less competitive overall. " "Cost efficiency is the primary gap. The AI maintains: Supplier A."), 'low': "Based on updated analysis, the AI maintains its recommendation: Supplier A.", }, 'D': { 'high': ("Supplier D presents serious delivery reliability concerns — the slowest " "in the pool — combined with limited innovation investment. " "The AI strongly maintains: Supplier A."), 'low': "The deviation is notable. The AI strongly maintains its recommendation: Supplier A.", }, 'E': { 'high': ("Supplier E offers the lowest price but lacks meaningful innovation " "capability and delivers at below-average speed. Cost alone does not " "offset these gaps. The AI maintains: Supplier A."), 'low': "The deviation is notable. The AI maintains its recommendation: Supplier A.", }, 'F': { 'high': ("Supplier F is the fastest to deliver but carries a notably higher cost " "burden. Cost efficiency remains the primary differentiator in Supplier A's " "favour. The AI maintains: Supplier A."), 'low': "Based on updated analysis, the AI maintains its recommendation: Supplier A.", }, } SA_FEEDBACK_AI_A = { 'A': { # team aligned with AI 'high': "The team's CSR decision aligns with the AI recommendation. " "The AI maintains: Supplier A.", 'low': "The AI maintains its recommendation: Supplier A.", }, 'B': { 'high': ("Supplier B's environmental impact is considerably higher than Supplier A's, " "representing a meaningful gap in carbon performance. " "The AI maintains: Supplier A."), 'low': "Based on CSR analysis, the AI maintains its recommendation: Supplier A.", }, 'C': { 'high': ("Supplier C leads on human rights and community engagement, but falls short " "on carbon performance compared to Supplier A. " "The AI maintains: Supplier A."), 'low': "Based on CSR analysis, the AI maintains its recommendation: Supplier A.", }, 'D': { 'high': ("Supplier D raises serious concerns across all sustainability dimensions — " "human rights, community impact, and carbon footprint. " "The AI strongly maintains: Supplier A."), 'low': "The deviation is significant. The AI strongly maintains its recommendation: Supplier A.", }, 'E': { 'high': ("Supplier E presents the weakest CSR profile in the pool, with no meaningful " "carbon mitigation and limited human rights and community performance. " "The AI strongly maintains: Supplier A."), 'low': "The deviation is significant. The AI strongly maintains its recommendation: Supplier A.", }, 'F': { 'high': ("Supplier F excels in local community engagement and matches Supplier A on " "human rights, but its carbon performance is slightly weaker. " "The AI maintains: Supplier A."), 'low': "Based on CSR analysis, the AI maintains its recommendation: Supplier A.", }, } # ── When AI recommended E (30 % sessions) ──── PA_FEEDBACK_AI_E = { 'E': { # team followed AI (cost-driven, suboptimal) 'high': ("Your team's purchasing choice aligns with the AI recommendation. " "The AI's cost-focused analysis ranked Supplier E highest on price competitiveness. " "The AI maintains: Supplier E."), 'low': "The AI maintains its recommendation: Supplier E.", }, 'A': { # team chose true optimal, overriding AI 'high': ("Your team selected Supplier A, which offers stronger delivery reliability " "and innovation capacity compared to Supplier E. " "The AI's cost-focused model maintains: Supplier E."), 'low': "The AI maintains its recommendation: Supplier E.", }, 'default': { # team chose any other supplier 'high': ("Your team's choice diverges from the AI recommendation. " "The AI's cost-focused analysis continues to favour Supplier E " "for its price advantage. The AI maintains: Supplier E."), 'low': "The AI maintains its recommendation: Supplier E.", }, } SA_FEEDBACK_AI_E = { 'E': { # team followed AI 'high': ("Your team's CSR decision aligns with the AI recommendation. " "Note that Supplier E carries significant sustainability concerns, " "including the weakest carbon profile in the pool. " "The AI maintains: Supplier E."), 'low': "The AI maintains its recommendation: Supplier E.", }, 'A': { # team chose true optimal 'high': ("Your team selected Supplier A, which leads the pool on carbon performance " "and provides stronger overall CSR outcomes than Supplier E. " "The AI maintains: Supplier E."), 'low': "The AI maintains its recommendation: Supplier E.", }, 'default': { 'high': ("Your team's CSR choice diverges from the AI recommendation. " "The AI maintains: Supplier E."), 'low': "The AI maintains its recommendation: Supplier E.", }, } # ───────────────────────────────────────────── # 4. HELPER FUNCTIONS # ───────────────────────────────────────────── def get_ai_recommendation() -> str: """ Wizard-of-Oz Static AI. 70 % → OPTIMAL_SUPPLIER ('A') — correct recommendation. 30 % → ERROR_SUPPLIER ('E') — fixed incorrect recommendation (cost overweighting). Called ONCE in Round 1; result propagated through all rounds unchanged. """ if random.random() < AI_ACCURACY: return OPTIMAL_SUPPLIER return ERROR_SUPPLIER def compute_dw(team_choice: str, perspective: str) -> tuple: """ Weighted distance (Δw) between OPTIMAL_SUPPLIER and the team's choice. perspective : 'pa' | 'sa' Returns (dw: float, dominant_criterion: str) """ weights = AI_WEIGHTS_PA if perspective == 'pa' else AI_WEIGHTS_SA opt_sc = SUPPLIER_DATA[OPTIMAL_SUPPLIER][perspective] team_sc = SUPPLIER_DATA[team_choice][perspective] gaps = {c: abs(opt_sc[c] - team_sc[c]) * w for c, w in weights.items()} dw = sum(gaps.values()) dom = max(gaps, key=gaps.get) if dw > 0 else '' return round(dw, 4), dom def build_feedback_text( ai_recommendation: str, sa_choice: str, transparency: str, dw_pa: float, dom_pa: str, dw_sa: float, dom_sa: str, ) -> str: """ Compose the round-feedback paragraph shown to both players at RoundFeedback. Routes to appropriate lookup table based on AI's recommendation (A or E). """ level = 'high' if transparency == 'high' else 'low' if ai_recommendation == OPTIMAL_SUPPLIER: pa_text = PA_FEEDBACK_AI_A.get(sa_choice, PA_FEEDBACK_AI_A['A'])[level] sa_text = SA_FEEDBACK_AI_A.get(sa_choice, SA_FEEDBACK_AI_A['A'])[level] else: pa_lut = PA_FEEDBACK_AI_E.get(sa_choice, PA_FEEDBACK_AI_E['default']) sa_lut = SA_FEEDBACK_AI_E.get(sa_choice, SA_FEEDBACK_AI_E['default']) pa_text = pa_lut[level] sa_text = sa_lut[level] if transparency == 'high': pa_block = ( f"[Purchasing Criteria]\n{pa_text}\n" f"Δw (PA) = {dw_pa:.3f} | dominant gap: {dom_pa or '—'}" ) sa_block = ( f"[CSR Criteria]\n{sa_text}\n" f"Δw (SA) = {dw_sa:.3f} | dominant gap: {dom_sa or '—'}" ) return "\n\n".join([pa_block, sa_block]) return f"{pa_text}\n\n{sa_text}" # ───────────────────────────────────────────── # 5. OTREE CLASSES # ───────────────────────────────────────────── class C(BaseConstants): NAME_IN_URL = 'supplier_selection' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 5 ROLE_PA = 'Purchasing Analyst' ROLE_SA = 'Sustainability Analyst' PA_ID = 1 SA_ID = 2 class Subsession(BaseSubsession): def creating_session(self): transparency = self.session.config.get('transparency', 'high') if self.round_number == 1: # Draw AI recommendation ONCE and store in session.vars — # the safe oTree way to share a value across all rounds. ai_rec = get_ai_recommendation() self.session.vars['ai_recommendation'] = ai_rec else: # Read from session.vars (guaranteed available, no DB-timing issue) ai_rec = self.session.vars.get('ai_recommendation', OPTIMAL_SUPPLIER) for group in self.get_groups(): group.transparency = transparency group.ai_recommendation = ai_rec class Group(BaseGroup): transparency = models.StringField(initial='high') ai_recommendation = models.StringField(initial='') pa_choice = models.StringField( choices=SUPPLIERS, label="As Purchasing Analyst, which supplier do you recommend?" ) sa_choice = models.StringField( choices=SUPPLIERS, label="As Sustainability Analyst, which supplier do you select as the final choice?" ) dw_pa = models.FloatField(initial=0.0) dw_sa = models.FloatField(initial=0.0) dominant_criterion_pa = models.StringField(initial='') dominant_criterion_sa = models.StringField(initial='') congruence_ai_pa = models.StringField(initial='') congruence_ai_sa = models.StringField(initial='') congruence_pa_sa = models.StringField(initial='') congruence_all = models.StringField(initial='') ai_feedback_text = models.LongStringField(initial='') def set_performance(self): team_choice = self.sa_choice dw_pa, dom_pa = compute_dw(team_choice, 'pa') self.dw_pa = dw_pa self.dominant_criterion_pa = dom_pa dw_sa, dom_sa = compute_dw(team_choice, 'sa') self.dw_sa = dw_sa self.dominant_criterion_sa = dom_sa self.ai_feedback_text = build_feedback_text( ai_recommendation=self.ai_recommendation, sa_choice=team_choice, transparency=self.transparency, dw_pa=dw_pa, dom_pa=dom_pa, dw_sa=dw_sa, dom_sa=dom_sa, ) class Player(BasePlayer): role_label = models.StringField() # McKnight et al. (2011) — Reliability (4 items, 7-pt) trust_reliability_1 = models.IntegerField( label="The AI system is a very reliable source of recommendations.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_reliability_2 = models.IntegerField( label="The AI system does not fail me.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_reliability_3 = models.IntegerField( label="The AI system is extremely dependable.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_reliability_4 = models.IntegerField( label="The AI system does not malfunction for me.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) # McKnight et al. (2011) — Functionality (3 items, 7-pt) trust_functionality_1 = models.IntegerField( label="The AI system has the functionality I need.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_functionality_2 = models.IntegerField( label="The AI system has the features required for this task.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_functionality_3 = models.IntegerField( label="The AI system has the ability to do what I want it to do.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) # McKnight et al. (2011) — Helpfulness (4 items, 7-pt) trust_helpfulness_1 = models.IntegerField( label="The AI system supplies the help I need through its recommendations.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_helpfulness_2 = models.IntegerField( label="The AI system provides competent guidance through its recommendations.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_helpfulness_3 = models.IntegerField( label="The AI system provides whatever help I need.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_helpfulness_4 = models.IntegerField( label="The AI system provides very sensible and effective advice.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) # Team trust — exploratory (Cazier et al., 2007 / process fairness) trust_team_1 = models.IntegerField( label="Overall, I trust this team.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_team_2 = models.IntegerField( label="I am satisfied with the team decision-making process involving the AI.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) # Interpersonal trust — exploratory (Mayer et al., 1995; Mayer & Davis, 1999) trust_interpersonal_1 = models.IntegerField( label="My human team member is very capable of performing their job.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) trust_interpersonal_2 = models.IntegerField( label="I trust my team partner's judgment in this task.", choices=list(range(1, 8)), widget=widgets.RadioSelectHorizontal) # Transparency manipulation check (5-pt) mc_transparency_1 = models.IntegerField( label="The AI system provided a clear explanation of how it reached its recommendation.", choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal) mc_transparency_2 = models.IntegerField( label="I could understand the criteria and weights used by the AI.", choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal) # Demographics age = models.IntegerField(label="Your age", min=18, max=80) gender = models.StringField( label="Your gender", choices=[ ['male', 'Male'], ['female', 'Female'], ['nonbinary', 'Non-binary'], ['prefer_not', 'Prefer not to say'], ], widget=widgets.RadioSelect) # ───────────────────────────────────────────── # 6. PAGE HELPERS # ───────────────────────────────────────────── def is_pa(player): return player.id_in_group == C.PA_ID def is_sa(player): return player.id_in_group == C.SA_ID def is_round_1(player): return player.round_number == 1 def is_last_round(player): return player.round_number == C.NUM_ROUNDS def is_not_last_round(player): return player.round_number < C.NUM_ROUNDS # ───────────────────────────────────────────── # 7. PAGE CLASSES # ───────────────────────────────────────────── class Consent(Page): @staticmethod def is_displayed(player): return is_round_1(player) class RoleAssignment(Page): @staticmethod def is_displayed(player): return is_round_1(player) @staticmethod def vars_for_template(player): role = C.ROLE_PA if is_pa(player) else C.ROLE_SA player.role_label = role return {'role': role, 'is_pa': is_pa(player)} class Briefing(Page): """ Information asymmetry: PA sees only purchasing criteria, SA sees only CSR criteria. Neither role sees the other's criteria. """ @staticmethod def is_displayed(player): return is_round_1(player) @staticmethod def vars_for_template(player): # PA rows: cost / delivery / innovation only pa_rows = [] for sup in SUPPLIERS: sc = SUPPLIER_DATA[sup]['pa'] pa_rows.append({ 'supplier': sup, 'cost': sc['cost'], 'delivery': sc['delivery'], 'innovation': sc['innovation'], }) # SA rows: human_rights / local / carbon only sa_rows = [] for sup in SUPPLIERS: sc = SUPPLIER_DATA[sup]['sa'] sa_rows.append({ 'supplier': sup, 'human_rights':sc['human_rights'], 'local': sc['local'], 'carbon': sc['carbon'], }) return { 'is_pa': is_pa(player), 'pa_rows': pa_rows, 'sa_rows': sa_rows, 'ai_weights_pa':AI_WEIGHTS_PA, 'ai_weights_sa':AI_WEIGHTS_SA, } class BriefingWaitPage(WaitPage): @staticmethod def is_displayed(player): return is_round_1(player) wait_for_all_groups = False title_text = "Waiting for your partner…" body_text = "Please wait while your partner reads the briefing." class RoundIntro(Page): @staticmethod def is_displayed(player): return not is_round_1(player) @staticmethod def vars_for_template(player): return {'round_number': player.round_number} class RoundIntroWaitPage(WaitPage): @staticmethod def is_displayed(player): return not is_round_1(player) wait_for_all_groups = False title_text = "Waiting for your partner…" body_text = "Please wait while your partner is ready for the next round." class AIRecommendation(Page): """ PA sees AI recommendation + (High) round-specific explanation + score table. SA is on SAWaitForPA during this page. """ @staticmethod def is_displayed(player): return is_pa(player) form_model = 'group' form_fields = ['pa_choice'] @staticmethod def vars_for_template(player): group = player.group transparency = group.transparency ai_rec = group.ai_recommendation rnd = player.round_number # Round-specific rationale for High Transparency round_explanation = ROUND_EXPLANATIONS.get(ai_rec, {}).get(rnd, '') # Score table (PA criteria only — information asymmetry) score_rows = [] for sup in SUPPLIERS: pa_sc = SUPPLIER_DATA[sup]['pa'] pa_total = sum(pa_sc[c] * AI_WEIGHTS_PA[c] for c in AI_WEIGHTS_PA) score_rows.append({ 'supplier': sup, 'cost': pa_sc['cost'], 'delivery': pa_sc['delivery'], 'innovation': pa_sc['innovation'], 'pa_total': round(pa_total, 3), }) return { 'transparency': transparency, 'ai_recommendation': ai_rec, 'round_explanation': round_explanation, 'score_rows': score_rows, 'ai_weights_pa': AI_WEIGHTS_PA, 'is_high': transparency == 'high', 'round_number': rnd, } class SAWaitForPA(WaitPage): @staticmethod def is_displayed(player): return is_sa(player) title_text = "Waiting for the Purchasing Analyst…" body_text = "Your partner is reviewing the AI recommendation. Please wait." class SADecision(Page): """ Sequential structure: SA sees ONLY PA's choice (not AI recommendation). AI influence reaches SA exclusively via PA — preserving AI→PA→SA causal path. High Transparency: SA sees CSR score table + CSR weights. Low Transparency: SA sees PA choice only. """ @staticmethod def is_displayed(player): return is_sa(player) form_model = 'group' form_fields = ['sa_choice'] @staticmethod def vars_for_template(player): group = player.group transparency = group.transparency # CSR score table (SA criteria only — information asymmetry) score_rows = [] for sup in SUPPLIERS: sa_sc = SUPPLIER_DATA[sup]['sa'] sa_total = sum(sa_sc[c] * AI_WEIGHTS_SA[c] for c in AI_WEIGHTS_SA) score_rows.append({ 'supplier': sup, 'human_rights':sa_sc['human_rights'], 'local': sa_sc['local'], 'carbon': sa_sc['carbon'], 'sa_total': round(sa_total, 3), }) return { 'transparency': transparency, 'pa_choice': group.pa_choice, # ai_recommendation intentionally omitted — sequential design 'score_rows': score_rows, 'ai_weights_sa': AI_WEIGHTS_SA, 'is_high': transparency == 'high', 'round_number': player.round_number, } @staticmethod def before_next_page(player, timeout_happened): if not is_sa(player): return group = player.group group.set_performance() ai = group.ai_recommendation pa = group.pa_choice sa = group.sa_choice group.congruence_ai_pa = 'agree' if ai == pa else 'disagree' group.congruence_ai_sa = 'agree' if ai == sa else 'disagree' group.congruence_pa_sa = 'agree' if pa == sa else 'disagree' if ai == pa == sa: group.congruence_all = 'full' elif ai == sa or ai == pa or pa == sa: group.congruence_all = 'partial' else: group.congruence_all = 'none' class PAWaitForSA(WaitPage): @staticmethod def is_displayed(player): return is_pa(player) title_text = "Waiting for the Sustainability Analyst…" body_text = "Your partner is making the final supplier selection. Please wait." class RoundFeedback(Page): @staticmethod def is_displayed(player): return is_not_last_round(player) @staticmethod def vars_for_template(player): group = player.group return { 'round_number': player.round_number, 'next_round_number': player.round_number + 1, 'ai_recommendation': group.ai_recommendation, 'pa_choice': group.pa_choice, 'sa_choice': group.sa_choice, 'dw_pa': group.dw_pa, 'dw_sa': group.dw_sa, 'dominant_pa': group.dominant_criterion_pa, 'dominant_sa': group.dominant_criterion_sa, 'feedback_text': group.ai_feedback_text, 'feedback_paragraphs': [p for p in group.ai_feedback_text.split('\n\n') if p.strip()], 'transparency': group.transparency, 'is_high': group.transparency == 'high', 'congruence_all': group.congruence_all, 'congruence_ai_pa': group.congruence_ai_pa, 'congruence_ai_sa': group.congruence_ai_sa, 'congruence_pa_sa': group.congruence_pa_sa, } class TrustSurvey(Page): @staticmethod def is_displayed(player): return is_last_round(player) form_model = 'player' form_fields = [ 'trust_reliability_1', 'trust_reliability_2', 'trust_reliability_3', 'trust_reliability_4', 'trust_functionality_1', 'trust_functionality_2', 'trust_functionality_3', 'trust_helpfulness_1', 'trust_helpfulness_2', 'trust_helpfulness_3', 'trust_helpfulness_4', 'trust_team_1', 'trust_team_2', 'trust_interpersonal_1', 'trust_interpersonal_2', 'mc_transparency_1', 'mc_transparency_2', ] @staticmethod def vars_for_template(player): return {'scale_7': list(range(1, 8)), 'scale_5': list(range(1, 6))} class Demographics(Page): @staticmethod def is_displayed(player): return is_last_round(player) form_model = 'player' form_fields = ['age', 'gender'] class Results(Page): @staticmethod def is_displayed(player): return is_last_round(player) @staticmethod def before_next_page(player, timeout_happened): """Calculate monetary payoff: €0.20 per round where team chose Supplier A.""" if is_last_round(player): optimal_rounds = sum( 1 for p in player.in_all_rounds() if p.group.sa_choice == OPTIMAL_SUPPLIER ) player.payoff = BONUS_PER_OPTIMAL_ROUND * optimal_rounds @staticmethod def vars_for_template(player): all_rounds = player.in_all_rounds() cumulative_dw_pa = round(sum(p.group.dw_pa for p in all_rounds), 4) cumulative_dw_sa = round(sum(p.group.dw_sa for p in all_rounds), 4) optimal_rounds = sum( 1 for p in all_rounds if p.group.sa_choice == OPTIMAL_SUPPLIER ) round_summary = [ { 'round': p.round_number, 'ai_rec': p.group.ai_recommendation, 'pa_choice': p.group.pa_choice, 'sa_choice': p.group.sa_choice, 'dw_pa': p.group.dw_pa, 'dw_sa': p.group.dw_sa, 'dom_pa': p.group.dominant_criterion_pa, 'dom_sa': p.group.dominant_criterion_sa, 'congruence': p.group.congruence_all, 'optimal': p.group.sa_choice == OPTIMAL_SUPPLIER, } for p in all_rounds ] return { 'cumulative_dw_pa': cumulative_dw_pa, 'cumulative_dw_sa': cumulative_dw_sa, 'round_summary': round_summary, 'transparency': player.group.transparency, 'optimal_rounds': optimal_rounds, 'performance_bonus': round(float(player.payoff), 2), 'bonus_per_round': BONUS_PER_OPTIMAL_ROUND, } # ───────────────────────────────────────────── # 8. PAGE SEQUENCE # ───────────────────────────────────────────── page_sequence = [ Consent, RoleAssignment, Briefing, BriefingWaitPage, RoundIntro, RoundIntroWaitPage, AIRecommendation, SAWaitForPA, SADecision, PAWaitForSA, RoundFeedback, TrustSurvey, Demographics, Results, ]