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. */}