Commit 4a78f686 by PLN (Algolia)

Integrate instrument attributions and the conversation guide into the couple app

parent e0607f29
......@@ -12,6 +12,7 @@
import { useMemo, useState } from 'react';
import { COUPLE_DATA } from './data';
import { compareAnswers, discussionAdvice, displayTitle } from './engine';
import { CONVERSATION_ETIQUETTE } from './attributions';
import { strings } from './strings';
import styles from './couple.module.css';
......@@ -37,14 +38,26 @@ export default function CompareView({ myQid, myRecord, theirBlock, onHome }) {
)}
</p>
{advice && (
<div className={styles.adviceBand}>
<p>{strings.compare.adviceTitle}</p>
<p><strong>{strings.compare.adviceTitle}</strong></p>
{advice && (
<p className={styles.muted}>
{strings.compare.adviceRule(advice.discussFrom, advice.noteBelow)}
</p>
</div>
)}
<p className={styles.muted}>{strings.compare.etiquetteTitle}</p>
<ul className={styles.etiquetteList}>
{CONVERSATION_ETIQUETTE.map(function (e) {
return <li key={e}>{e}</li>;
})}
</ul>
{advice && q.scoring && q.scoring.debrief && (
<details className={styles.attribDetails}>
<summary className={styles.attribSummary}>{strings.compare.fullAdvice}</summary>
<p className={styles.muted}>{q.scoring.debrief}</p>
</details>
)}
</div>
<table className={styles.compareTable}>
<thead>
......
......@@ -23,6 +23,7 @@ import {
writeSession,
} from './engine';
import { exportCouplePdf } from './pdf-export';
import { ATTRIBUTIONS, RIGHTS_LINE } from './attributions';
import { strings } from './strings';
import styles from './couple.module.css';
......@@ -151,6 +152,19 @@ export default function HomeView({ goIntro, goQuiz, goRecap, trackWrite, goCompa
<footer className={styles.pagefoot}>
<p>{strings.home.footLine}</p>
<p className={styles.muted}>{RIGHTS_LINE}</p>
<details className={styles.attribDetails}>
<summary className={styles.attribSummary}>{strings.home.creditsTitle}</summary>
{COUPLE_DATA.questionnaires.map(function (q) {
var a = ATTRIBUTIONS[q.id];
if (!a) return null;
return (
<p key={q.id} className={styles.muted}>
<strong>{displayTitle(q.title)}</strong> : {a.source}
</p>
);
})}
</details>
<p className={styles.footlinks}>
{confirmingDelete ? (
<>
......
......@@ -9,6 +9,7 @@
import { useState } from 'react';
import Button from '@/components/clarte/Button';
import { answeredCount, displayTitle, firstUnanswered, readRecord, writeRecord } from './engine';
import { ATTRIBUTIONS } from './attributions';
import { strings } from './strings';
import styles from './couple.module.css';
......@@ -68,6 +69,18 @@ export default function IntroView({ q, goHome, goQuiz, goRecap, trackWrite = fun
{ctaLabel}
</Button>
</div>
<p className={styles.muted}>{(ATTRIBUTIONS[q.id] || {}).line}</p>
{(ATTRIBUTIONS[q.id] || {}).source && (
<details className={styles.attribDetails}>
<summary className={styles.attribSummary}>{strings.intro.originTitle}</summary>
<p className={styles.muted}>{ATTRIBUTIONS[q.id].source}</p>
{ATTRIBUTIONS[q.id].url && (
<p className={styles.muted}>
<a href={ATTRIBUTIONS[q.id].url} target="_blank" rel="noreferrer">source publiée par l’auteur</a>
</p>
)}
</details>
)}
{q.intro && <p>{q.intro}</p>}
{q.instructions && <p>{q.instructions}</p>}
......
......@@ -7,6 +7,7 @@
import { useMemo } from 'react';
import { COUPLE_DATA } from './data';
import { displayTitle } from './engine';
import { RIGHTS_LINE } from './attributions';
import { strings } from './strings';
import styles from './couple.module.css';
......@@ -74,6 +75,7 @@ export default function SharedAllView({ payload }) {
})}
<p className={styles.muted}>{strings.shared.foot}</p>
<p className={styles.muted}>{RIGHTS_LINE}</p>
</div>
);
}
......@@ -8,6 +8,7 @@
import { COUPLE_DATA } from './data';
import { allItems, displayTitle, optionLabel } from './engine';
import { strings } from './strings';
import { RIGHTS_LINE } from './attributions';
import styles from './couple.module.css';
export default function SharedView({ payload }) {
......@@ -72,6 +73,7 @@ export function SharedErrorView() {
{strings.shared.backHome}
</a>
</p>
<p className={styles.muted}>{RIGHTS_LINE}</p>
</div>
);
}
/* Clarté couple — attributions des instruments. Contenu RÉDIGÉ PAR L'APP
(pas la donnée clinique) : la fiche de crédit de chaque questionnaire,
établie par l'enquête de provenance (Diag_ADHD/redesign/scale-provenance-
online.md, 2026-09-23, chaque source vérifiée ou marquée). data.js reste
intouché : on cite, on ne modifie jamais les textes d'origine. */
export const RIGHTS_LINE =
'Tous droits réservés à leurs auteurs respectifs. Tests présentés à titre informatif, sans valeur diagnostique.';
export const ATTRIBUTIONS = {
bonheur_conjugal: {
line: 'De Yvon Dallaire, psychologue (Québec).',
source:
'© Yvon Dallaire. Publié par l’auteur sur yvondallaire.com (test du bonheur conjugal, PDF libre). Les bandes d’interprétation sont les siennes, conservées telles quelles.',
url: 'https://yvondallaire.com/tests/test-de-bonheur-conjugal/',
},
satisfaction_sexuelle: {
line: 'De Yvon Dallaire, psychologue (Québec).',
source:
'© Yvon Dallaire. Publié par l’auteur sur yvondallaire.com (test de satisfaction sexuelle, PDF libre). Le support indique lui-même qu’il s’inspire des sexologues Djamel Benois et Olivier Walmacq.',
url: 'https://yvondallaire.com/tests/test-de-satisfaction-sexuelle/',
},
pensees_negatives: {
line: 'Instrument clinique : élaboré par Trinque (2000) sous la direction de Gilles Trudel (UQAM).',
source:
'Annexe de G. Trudel, « La baisse du désir sexuel », Elsevier Masson (ISBN 978-2-294-00999-5). Aucune copie publique n’existe : le texte vit dans ce support privé.',
},
communication: {
line: 'De Jean-Marie Boisvert et Josette Beaudry.',
source:
'Jean-Marie Boisvert et Josette Beaudry, « S’affirmer et communiquer », Presses de l’Université de Montréal (ISBN 978-2-7619-3525-8, grille de communication affirmative). Grille élargie à quatorze items pour ce support.',
},
inhibiteurs_excitateurs: {
line: 'Adaptation française de l’échelle d’Emily Nagoski, elle-même abrégée du SESII-W (Bancroft & Janssen).',
source:
'© 2015 Emily Nagoski, « Sexual Temperament Questionnaire », adapté et abrégé du SESII-W. Feuilles d’origine sur emilynagoski.com. Traduction libre : la version française de ce support n’est pas la traduction publiée.',
url: 'https://www.emilynagoski.com/come-as-you-are-worksheets',
},
desir_relationnel_comportemental: {
line: 'Instrument clinique : élaboré par Drouin (1999) sous la direction de Gilles Trudel (UQAM).',
source:
'Annexe de G. Trudel, « La baisse du désir sexuel », Elsevier Masson (ISBN 978-2-294-00999-5). Trace en ligne vérifiable : la page professorale de G. Trudel, UQAM.',
},
desir_individuel_cognitif: {
line: 'Instrument clinique : élaboré par Trinque (2000) sous la direction de Gilles Trudel (UQAM).',
source:
'Annexe de G. Trudel, « La baisse du désir sexuel », Elsevier Masson (ISBN 978-2-294-00999-5). Validé sur échantillon de femmes : le support d’origine le note, l’app le rappelle dans son introduction.',
},
folio_vie_en_couple: {
line: 'Feuille de pratique : auteur non identifié.',
source:
'Feuille de travail trouvée dans les supports de la praticienne, sans auteur identifiable. Sept questions à réponses écrites, sans cotation.',
},
};
/* L'étiquette de conversation, propre à l'app : le support de Dallaire pose
la règle la plus concrète (discuter les 3 et 4, noter les 0-2, chercher
« coupable » n'a jamais rien arrangé) ; les autres instruments n'écrivent
rien de tel, donc l'app offre une etiquette générique, clairement portée
par elle et non attribuée à l'instrument. */
export const CONVERSATION_ETIQUETTE = [
'Commencer par ce qui va : les réponses satisfaites se disent avant tout.',
'Une question à la fois, sans enchaîner vers les autres sujets.',
'Les réponses qui piquent se notent pour adapter son comportement, pas pour débattre de qui a raison.',
'Une réponse décrit un moment, comme une photo : ce n’est ni un verdict ni un contrat.',
];
......@@ -431,3 +431,12 @@
.rowDivergent { background: #fff; font-weight: 700; }
.adviceBand { background: var(--teal-soft); border-radius: var(--radius); padding: var(--sp-2) var(--sp-3); margin: var(--sp-3) 0; }
.adviceBand p { margin: 0; font-size: var(--fs-small); }
/* Attributions et guide de conversation. */
.attribDetails { margin: var(--sp-1) 0; }
.attribSummary { color: var(--ink-faint); font-size: var(--fs-caption); cursor: pointer; }
.attribSummary:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.attribDetails p { font-size: var(--fs-caption); margin: var(--sp-1) 0; }
.attribDetails a { color: var(--teal-text); }
.etiquetteList { margin: var(--sp-1) 0; padding-left: 1.1rem; }
.etiquetteList li { font-size: var(--fs-small); margin: var(--sp-1) 0; }
......@@ -34,6 +34,9 @@ export const strings = {
'Ce lien regroupe tout le parcours sur une seule page, en lecture seule pour qui l’ouvre. Même prudence que pour un lien simple.',
pdfBtn: 'Exporter tout en PDF (documents remplis)',
pdfDone: 'PDF téléchargé',
creditsTitle: 'Crédits et sources',
rightsLine:
'Tous droits réservés à leurs auteurs respectifs. Tests présentés à titre informatif, sans valeur diagnostique.',
},
intro: {
......@@ -46,6 +49,7 @@ export const strings = {
resume: (firstUnanswered1Based) => 'Reprendre à la question ' + firstUnanswered1Based,
seeRecap: 'Voir mes réponses',
more: 'En savoir plus',
originTitle: 'D’où vient ce questionnaire ?',
},
quiz: {
......@@ -142,10 +146,12 @@ export const strings = {
colTheirs: 'L’autre',
moi: 'cet appareil',
autre: 'l’autre',
adviceTitle: 'Le conseil du support d’origine pour en parler :',
adviceTitle: 'Comment en parler',
adviceRule: (from, below) =>
'Les réponses à ' + from + ' ou 4 sont faites pour être dites ; les réponses sous ' + below +
'Le conseil du support d’origine (Yvon Dallaire) : les réponses à ' + from + ' ou 4 sont faites pour être dites ; celles sous ' + below +
' se notent pour adapter son comportement, sans en faire un débat.',
etiquetteTitle: 'Quelques habitudes qui aident (de l’app, pas du questionnaire) :',
fullAdvice: 'Le texte complet du support d’origine',
pcts: (mine, theirs) =>
'Scores de l’instrument : ' + (mine || '—') + ' (moi) et ' + (theirs || '—') + (theirs ? '' : '') + '.',
noScore:
......
......@@ -72,3 +72,35 @@ describe('compareAnswers — divergence par item, jamais un score de couple', ()
expect(rows[1].divergence).toBe(false);
});
});
/* ---------- attributions : chaque instrument crédité ---------- */
import { ATTRIBUTIONS, RIGHTS_LINE, CONVERSATION_ETIQUETTE } from '@/components/couple/attributions';
describe('attributions — provenance intégrale', () => {
it('every questionnaire has a credit entry with a line and a source', () => {
const missing = [];
COUPLE_DATA.questionnaires.forEach((q) => {
const a = ATTRIBUTIONS[q.id];
if (!a || !a.line || !a.source) missing.push(q.id);
});
expect(missing).toEqual([]);
});
it('credit entries match the research sheet (spot checks)', () => {
expect(ATTRIBUTIONS.bonheur_conjugal.source).toContain('yvondallaire.com');
expect(ATTRIBUTIONS.inhibiteurs_excitateurs.source).toContain('Nagoski');
expect(ATTRIBUTIONS.inhibiteurs_excitateurs.source).toContain('SESII-W');
expect(ATTRIBUTIONS.communication.source).toContain('Boisvert');
expect(ATTRIBUTIONS.pensees_negatives.source).toContain('Elsevier Masson');
expect(ATTRIBUTIONS.folio_vie_en_couple.line).toContain('non identifié');
});
it('rights line and etiquette carry no em dashes', () => {
expect(RIGHTS_LINE.includes('—')).toBe(false);
CONVERSATION_ETIQUETTE.forEach((e) => expect(e.includes('—')).toBe(false));
Object.values(ATTRIBUTIONS).forEach((a) => {
expect(a.line.includes('—')).toBe(false);
expect(a.source.includes('—')).toBe(false);
});
});
});
......@@ -73,9 +73,9 @@ describe('HomeView', () => {
it('renders the 8 questionnaire cards with fresh meta (folio: écrits à compléter)', async () => {
setup();
await act(async () => {});
const folioCard = screen.getByText('La vie en couple').closest('button');
const folioCard = screen.getAllByText('La vie en couple')[0].closest('button');
expect(folioCard.textContent).toContain('7 écrits à compléter');
const bonheurCard = screen.getByText('Test du bonheur conjugal').closest('button');
const bonheurCard = screen.getAllByText('Test du bonheur conjugal')[0].closest('button');
expect(bonheurCard.textContent).toContain('25 questions');
});
......@@ -95,14 +95,14 @@ describe('HomeView', () => {
it('routes fresh cards to intro, partial to firstUnanswered, complete to recap', async () => {
let mocks = setup();
await act(async () => {});
screen.getByText('Test du bonheur conjugal').closest('button').click();
screen.getAllByText('Test du bonheur conjugal')[0].closest('button').click();
expect(mocks.goIntro).toHaveBeenCalledWith('bonheur_conjugal');
cleanup();
seed('bonheur_conjugal', { answers: { 1: 4, 2: 2, 3: 0 }, pos: 0 });
mocks = setup();
await act(async () => {});
const partialCard = screen.getByText('Test du bonheur conjugal').closest('button');
const partialCard = screen.getAllByText('Test du bonheur conjugal')[0].closest('button');
expect(partialCard.textContent).toContain('En cours · 3 sur 25');
partialCard.click();
expect(mocks.goQuiz).toHaveBeenCalledWith('bonheur_conjugal', 3);
......@@ -111,7 +111,7 @@ describe('HomeView', () => {
seed('bonheur_conjugal', { answers: fullAnswers(25, 3), pos: 25 });
mocks = setup();
await act(async () => {});
const doneCard = screen.getByText('Test du bonheur conjugal').closest('button');
const doneCard = screen.getAllByText('Test du bonheur conjugal')[0].closest('button');
expect(doneCard.textContent).toContain('Terminé · voir mes réponses');
doneCard.click();
expect(mocks.goRecap).toHaveBeenCalledWith('bonheur_conjugal');
......@@ -522,6 +522,6 @@ describe('ClarteCouple — hash dispatch', () => {
it('no hash boots to home with 8 cards', async () => {
render(<ClarteCouple />);
expect(await screen.findByText('Espace couple')).toBeInTheDocument();
expect(screen.getByText('Test du bonheur conjugal')).toBeInTheDocument();
expect(screen.getAllByText('Test du bonheur conjugal')[0]).toBeInTheDocument();
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment