From eda0b5831d50075efc79c83d2431b3a0e0227e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 5 Oct 2026 08:11:10 +0200 Subject: [PATCH] feat(settings): summarise what the Coach sees beside the link to its settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → Coach pointed at the gear with a muted paragraph. It now says how many data areas the Coach reads and how far back it looks, from the saved preferences, next to an Open Coach settings button that lands on that section. --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 3 +- messages/fr.json | 3 +- messages/it.json | 3 +- messages/ko.json | 3 +- messages/pl.json | 3 +- .../coach-prefs-sources-summary.test.tsx | 76 +++++++++++++++++++ .../settings/coach-prefs-section.tsx | 69 +++++++++++++---- 9 files changed, 144 insertions(+), 22 deletions(-) create mode 100644 src/components/settings/__tests__/coach-prefs-sources-summary.test.tsx diff --git a/messages/de.json b/messages/de.json index 334ca82d0..f15838457 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2941,7 +2941,8 @@ "settingsVerbosityDefault": "Standard", "settingsVerbosityDetailed": "Ausführlich", "settingsVerbosityConciseHint": "Der knappe Tonfall hält Antworten ohnehin kurz — die Ausführlichkeit ist dabei fest auf „Kurz“ gesetzt.", - "settingsSourcesPointer": "Welche Daten der Coach lesen darf und wie weit er zurückblickt, stellst du im Coach selbst ein, hinter dem Zahnrad („Worauf ich zugreife“).", + "settingsSourcesSummary": "Der Coach sieht {count} von {total} Datenbereichen und blickt {window} zurück.", + "settingsSourcesSummaryAll": "Der Coach sieht {count} von {total} Datenbereichen und deinen ganzen Verlauf.", "settingsSourcesPointerLink": "Coach-Einstellungen öffnen", "settingsExcludeLabel": "Metriken ausschließen", "settingsExcludeHint": "Der Coach sieht keine Daten der ausgewählten Metriken.", diff --git a/messages/en.json b/messages/en.json index 6287e5518..b4193d4af 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2941,7 +2941,8 @@ "settingsVerbosityDefault": "Default", "settingsVerbosityDetailed": "Detailed", "settingsVerbosityConciseHint": "The concise tone already keeps replies short — verbosity is pinned to Brief while it is active.", - "settingsSourcesPointer": "Which data the Coach may read and how far back it looks are set in the Coach itself, behind the gear (\"What I can see\").", + "settingsSourcesSummary": "The Coach sees {count} of {total} data areas and looks back {window}.", + "settingsSourcesSummaryAll": "The Coach sees {count} of {total} data areas and your whole history.", "settingsSourcesPointerLink": "Open Coach settings", "settingsExcludeLabel": "Exclude metrics", "settingsExcludeHint": "The Coach will not see data for selected metrics.", diff --git a/messages/es.json b/messages/es.json index d276a21c1..7e14e6e10 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2941,7 +2941,8 @@ "settingsVerbosityDefault": "Estándar", "settingsVerbosityDetailed": "Detallado", "settingsVerbosityConciseHint": "El tono conciso ya mantiene las respuestas cortas: la extensión queda fijada en «Breve» mientras esté activo.", - "settingsSourcesPointer": "Qué datos puede leer el coach y hasta cuándo mira atrás se ajusta en el propio coach, tras el engranaje («A qué accedo»).", + "settingsSourcesSummary": "El coach ve {count} de {total} áreas de datos y mira {window} atrás.", + "settingsSourcesSummaryAll": "El coach ve {count} de {total} áreas de datos y todo el historial.", "settingsSourcesPointerLink": "Abrir ajustes del coach", "settingsExcludeLabel": "Excluir métricas", "settingsExcludeHint": "El coach no verá los datos de las métricas seleccionadas.", diff --git a/messages/fr.json b/messages/fr.json index bdc8fe468..7b47b0537 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2941,7 +2941,8 @@ "settingsVerbosityDefault": "Standard", "settingsVerbosityDetailed": "Détaillé", "settingsVerbosityConciseHint": "Le ton concis garde déjà les réponses courtes — la longueur reste fixée sur « Bref » tant qu'il est actif.", - "settingsSourcesPointer": "Les données que le coach peut lire et jusqu’où il remonte se règlent dans le coach lui-même, derrière la roue dentée (« Ce à quoi j’accède »).", + "settingsSourcesSummary": "Le coach voit {count} domaines de données sur {total} et remonte sur {window}.", + "settingsSourcesSummaryAll": "Le coach voit {count} domaines de données sur {total} et tout l’historique.", "settingsSourcesPointerLink": "Ouvrir les paramètres du coach", "settingsExcludeLabel": "Exclure des métriques", "settingsExcludeHint": "Le coach ne verra pas les données des métriques sélectionnées.", diff --git a/messages/it.json b/messages/it.json index f58556352..576e2f6a0 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2941,7 +2941,8 @@ "settingsVerbosityDefault": "Standard", "settingsVerbosityDetailed": "Dettagliata", "settingsVerbosityConciseHint": "Il tono conciso mantiene già le risposte brevi: la verbosità resta fissata su «Breve» finché è attivo.", - "settingsSourcesPointer": "Quali dati il coach può leggere e fin dove guarda indietro si imposta nel coach stesso, dietro l’ingranaggio («A cosa accedo»).", + "settingsSourcesSummary": "Il coach vede {count} aree di dati su {total} e guarda indietro di {window}.", + "settingsSourcesSummaryAll": "Il coach vede {count} aree di dati su {total} e l’intero storico.", "settingsSourcesPointerLink": "Apri le impostazioni del coach", "settingsExcludeLabel": "Escludi metriche", "settingsExcludeHint": "Il coach non vede i dati delle metriche selezionate.", diff --git a/messages/ko.json b/messages/ko.json index 9fb976c03..1787f8272 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -2941,7 +2941,8 @@ "settingsVerbosityDefault": "기본", "settingsVerbosityDetailed": "자세히", "settingsVerbosityConciseHint": "간결한 말투는 이미 답변을 짧게 유지해요 — 켜 두는 동안 답변 길이는 '짧게'로 고정돼요.", - "settingsSourcesPointer": "코치가 읽을 수 있는 데이터와 되돌아볼 기간은 코치 안의 톱니바퀴('내가 볼 수 있는 것')에서 정해요.", + "settingsSourcesSummary": "코치는 데이터 영역 {total}개 중 {count}개를 {window} 전까지 봐요.", + "settingsSourcesSummaryAll": "코치는 데이터 영역 {total}개 중 {count}개를 처음 기록부터 봐요.", "settingsSourcesPointerLink": "코치 설정 열기", "settingsExcludeLabel": "제외할 지표", "settingsExcludeHint": "선택한 지표의 데이터는 코치가 보지 않아요.", diff --git a/messages/pl.json b/messages/pl.json index 98c255698..46ab15664 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2941,7 +2941,8 @@ "settingsVerbosityDefault": "Standardowa", "settingsVerbosityDetailed": "Szczegółowa", "settingsVerbosityConciseHint": "Zwięzły ton i tak skraca odpowiedzi — szczegółowość pozostaje ustawiona na „Krótko”, dopóki jest aktywny.", - "settingsSourcesPointer": "To, jakie dane coach może czytać i jak daleko wstecz patrzy, ustawisz w samym coachu, pod kołem zębatym („Do czego mam dostęp”).", + "settingsSourcesSummary": "Coach widzi {count} z {total} obszarów danych i patrzy {window} wstecz.", + "settingsSourcesSummaryAll": "Coach widzi {count} z {total} obszarów danych i całą historię.", "settingsSourcesPointerLink": "Otwórz ustawienia coacha", "settingsExcludeLabel": "Wyklucz metryki", "settingsExcludeHint": "Coach nie zobaczy danych wybranych metryk.", diff --git a/src/components/settings/__tests__/coach-prefs-sources-summary.test.tsx b/src/components/settings/__tests__/coach-prefs-sources-summary.test.tsx new file mode 100644 index 000000000..a1d4158a6 --- /dev/null +++ b/src/components/settings/__tests__/coach-prefs-sources-summary.test.tsx @@ -0,0 +1,76 @@ +/** + * Settings → Coach says what the Coach reads at the saved setting, in one + * sentence, beside the link into the Coach's own settings: how many data + * areas are on, out of how many, and how far back it looks. + */ +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@/hooks/use-ai-capability", () => ({ + useAiCapability: () => ({ + available: true, + reason: null, + onDeviceAllowed: false, + }), +})); +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), + usePathname: () => "/settings/coach", +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { queryKeys } from "@/lib/query-keys"; +import { + DEFAULT_COACH_CLUSTERS, + DEFAULT_COACH_PREFS, + coachDataClusterEnum, + type CoachPrefs, +} from "@/lib/validations/coach-prefs"; +import { CoachPrefsSection } from "../coach-prefs-section"; + +function summary(prefs: CoachPrefs): string { + const client = new QueryClient({ + defaultOptions: { queries: { retry: 0, staleTime: Infinity } }, + }); + client.setQueryData(queryKeys.coachPrefs(), prefs); + const html = renderToStaticMarkup( + + + + + , + ); + const match = html.match( + /data-slot="coach-prefs-sources-summary"[^>]*>([^<]*) { + it("counts the default areas and says the whole history is read", () => { + expect(summary(DEFAULT_COACH_PREFS)).toBe( + `The Coach sees ${DEFAULT_COACH_CLUSTERS.length} of ${TOTAL} data areas and your whole history.`, + ); + }); + + it("names a bounded lookback in the picker's words", () => { + expect( + summary({ + ...DEFAULT_COACH_PREFS, + dataClusters: ["cardio", "sleep"], + defaultWindow: "last90days", + }), + ).toBe(`The Coach sees 2 of ${TOTAL} data areas and looks back 90 days.`); + expect( + summary({ + ...DEFAULT_COACH_PREFS, + dataClusters: [], + defaultWindow: "lastYear", + }), + ).toBe(`The Coach sees 0 of ${TOTAL} data areas and looks back 12 months.`); + }); +}); diff --git a/src/components/settings/coach-prefs-section.tsx b/src/components/settings/coach-prefs-section.tsx index dfced9eb9..1e1756575 100644 --- a/src/components/settings/coach-prefs-section.tsx +++ b/src/components/settings/coach-prefs-section.tsx @@ -41,8 +41,11 @@ import { useTranslations } from "@/lib/i18n/context"; import { useCoachPrefs, useSaveCoachPrefs } from "@/hooks/use-coach-prefs"; import { useAiCapability } from "@/hooks/use-ai-capability"; import { COACH_FOLLOW_UP_UI_KEYS } from "@/lib/ai/coach/dialog-keys"; +import { HISTORY_REACH_DAYS, lookbackText } from "@/lib/ai/coach/history-reach"; import { + DEFAULT_COACH_CLUSTERS, DEFAULT_COACH_PREFS, + coachDataClusterEnum, type CoachExcludeMetric, type CoachPrefs, type CoachTone, @@ -70,6 +73,32 @@ const CONTEXT_OPTIONS: CoachExcludeMetric[] = [ "anthropometrics", ]; +/** + * One sentence on what the Coach reads at the saved setting: how many of the + * data areas are on and how far back it looks, in the words the lookback + * picker uses. + */ +function sourcesSummary( + prefs: CoachPrefs, + t: ReturnType["t"], +): string { + const params = { + count: (prefs.dataClusters ?? DEFAULT_COACH_CLUSTERS).length, + total: coachDataClusterEnum.options.length, + }; + const window = prefs.defaultWindow; + if (HISTORY_REACH_DAYS[window] === null) { + return t("insights.coach.settingsSourcesSummaryAll", params); + } + return t("insights.coach.settingsSourcesSummary", { + ...params, + window: t( + `insights.coach.lookback.option.${window}`, + lookbackText(window).optionParams, + ), + }); +} + export interface CoachPrefsSectionProps { isAuthenticated: boolean; } @@ -225,25 +254,35 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) { {/* Data clusters + analysis window live on ONE owner: "What I can see" behind the Coach's settings gear, which - `/coach?settings=data` opens directly. This card - used to render its own copies of both controls; two writable - surfaces for the same persisted fields is exactly the - redundancy the rail was built to end. A pointer replaces - them. Only while the Coach can open: without it the link - would bounce back to Insights and the rail is out of reach. */} + `/coach?settings=data` opens directly. This card used to render + its own copies of both controls; two writable surfaces for the + same persisted fields is exactly the redundancy the rail was + built to end. It says what the Coach sees at the saved setting + and links there. Only while the Coach can open: without it the + link would bounce back to Insights and the rail is out of + reach. */} {coach.available ? ( -

- {t("insights.coach.settingsSourcesPointer")}{" "} - - {t("insights.coach.settingsSourcesPointerLink")} - -

+ {sourcesSummary(draft, t)} +

+ + ) : null} {/* Exclude metrics — checked = excluded. */}