feat(settings): summarise what the Coach sees beside the link to its settings

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.
This commit is contained in:
Marc-André Bombeck committed 2026-10-05 08:11:10 +02:00
1 parent 816269b3ed
commit eda0b5831d
9 files changed
+144 -22

No files matched your search

+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -2941,7 +2941,8 @@
"settingsVerbosityDefault": "기본",
"settingsVerbosityDetailed": "자세히",
"settingsVerbosityConciseHint": "간결한 말투는 이미 답변을 짧게 유지해요 — 켜 두는 동안 답변 길이는 '짧게'로 고정돼요.",
"settingsSourcesPointer": "코치가 읽을 수 있는 데이터와 되돌아볼 기간은 코치 안의 톱니바퀴('내가 볼 수 있는 것')에서 정해요.",
"settingsSourcesSummary": "코치는 데이터 영역 {total}개 중 {count}개를 {window} 전까지 봐요.",
"settingsSourcesSummaryAll": "코치는 데이터 영역 {total}개 중 {count}개를 처음 기록부터 봐요.",
"settingsSourcesPointerLink": "코치 설정 열기",
"settingsExcludeLabel": "제외할 지표",
"settingsExcludeHint": "선택한 지표의 데이터는 코치가 보지 않아요.",
+2 -1
View File
@@ -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.",
@@ -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(
<QueryClientProvider client={client}>
<I18nProvider initialLocale="en">
<CoachPrefsSection isAuthenticated />
</I18nProvider>
</QueryClientProvider>,
);
const match = html.match(
/data-slot="coach-prefs-sources-summary"[^>]*>([^<]*)</,
);
expect(match, "summary rendered").not.toBeNull();
return match![1];
}
const TOTAL = coachDataClusterEnum.options.length;
describe("Settings → Coach 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.`);
});
});
+54 -15
View File
@@ -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<typeof useTranslations>["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 ? (
<p
<div
data-slot="coach-prefs-sources-pointer"
className="text-muted-foreground border-border/60 rounded-md border border-dashed px-3 py-2 text-xs leading-relaxed"
className="flex items-start justify-between gap-3"
>
{t("insights.coach.settingsSourcesPointer")}{" "}
<Link
href="/coach?settings=data"
className="text-foreground underline underline-offset-2"
<p
data-slot="coach-prefs-sources-summary"
className="min-w-0 flex-1 text-sm"
>
{t("insights.coach.settingsSourcesPointerLink")}
</Link>
</p>
{sourcesSummary(draft, t)}
</p>
<Button
asChild
variant="outline"
size="sm"
className="min-h-11 shrink-0 sm:min-h-9"
>
<Link href="/coach?settings=data">
{t("insights.coach.settingsSourcesPointerLink")}
</Link>
</Button>
</div>
) : null}
{/* Exclude metrics — checked = excluded. */}