diff --git a/messages/de.json b/messages/de.json index 146bbed3a..9307de5f8 100644 --- a/messages/de.json +++ b/messages/de.json @@ -6107,7 +6107,10 @@ "frequencyAria": "Häufigkeit der Coach-Hinweise wählen", "enabledToast": "Proaktive Coach-Hinweise aktiviert.", "disabledToast": "Proaktive Coach-Hinweise deaktiviert.", - "saveError": "Speichern fehlgeschlagen. Bitte später erneut versuchen." + "saveError": "Speichern fehlgeschlagen. Bitte später erneut versuchen.", + "suggestionsLabel": "Beispielvorschläge", + "suggestionsDesc": "Zeigt auf dem Coach-Bildschirm gelegentlich eine fertige Beispielfrage.", + "suggestionsAria": "Beispiel-Coach-Vorschläge umschalten" }, "lowStock": { "title": "Vorratswarnung", diff --git a/messages/en.json b/messages/en.json index 23ae4f2b7..716477da3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -6107,7 +6107,10 @@ "frequencyAria": "Select nudge frequency", "enabledToast": "Proactive Coach nudges enabled.", "disabledToast": "Proactive Coach nudges disabled.", - "saveError": "Could not save. Please try again later." + "saveError": "Could not save. Please try again later.", + "suggestionsLabel": "Example suggestions", + "suggestionsDesc": "Show the occasional ready-made example question on the Coach screen.", + "suggestionsAria": "Toggle example Coach suggestions" }, "lowStock": { "title": "Low-stock alert", diff --git a/messages/es.json b/messages/es.json index e2f12b186..fe9e716ea 100644 --- a/messages/es.json +++ b/messages/es.json @@ -6107,7 +6107,10 @@ "statusOff": "Desactivado", "enabledToast": "Avisos proactivos del Coach activados.", "disabledToast": "Avisos proactivos del Coach desactivados.", - "saveError": "No se pudo guardar. Inténtalo de nuevo más tarde." + "saveError": "No se pudo guardar. Inténtalo de nuevo más tarde.", + "suggestionsLabel": "Sugerencias de ejemplo", + "suggestionsDesc": "Muestra de vez en cuando una pregunta de ejemplo en la pantalla del Coach.", + "suggestionsAria": "Activar o desactivar las sugerencias de ejemplo del Coach" }, "lowStock": { "title": "Aviso de existencias bajas", diff --git a/messages/fr.json b/messages/fr.json index 5b953e533..46874950c 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -6107,7 +6107,10 @@ "statusOff": "Désactivé", "enabledToast": "Rappels proactifs du Coach activés.", "disabledToast": "Rappels proactifs du Coach désactivés.", - "saveError": "Impossible d'enregistrer. Veuillez réessayer plus tard." + "saveError": "Impossible d'enregistrer. Veuillez réessayer plus tard.", + "suggestionsLabel": "Suggestions d'exemples", + "suggestionsDesc": "Affiche de temps en temps une question d'exemple prête à l'emploi sur l'écran du Coach.", + "suggestionsAria": "Activer ou désactiver les suggestions d'exemples du Coach" }, "lowStock": { "title": "Alerte de stock faible", diff --git a/messages/it.json b/messages/it.json index 1c446bda3..8eda2e620 100644 --- a/messages/it.json +++ b/messages/it.json @@ -6107,7 +6107,10 @@ "statusOff": "Disattivato", "enabledToast": "Avvisi proattivi del Coach attivati.", "disabledToast": "Avvisi proattivi del Coach disattivati.", - "saveError": "Impossibile salvare. Riprova più tardi." + "saveError": "Impossibile salvare. Riprova più tardi.", + "suggestionsLabel": "Suggerimenti di esempio", + "suggestionsDesc": "Mostra di tanto in tanto una domanda di esempio pronta all'uso nella schermata del Coach.", + "suggestionsAria": "Attiva o disattiva i suggerimenti di esempio del Coach" }, "lowStock": { "title": "Avviso scorte basse", diff --git a/messages/pl.json b/messages/pl.json index cb6fd0d43..9bbc6dc27 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -6107,7 +6107,10 @@ "statusOff": "Wyłączone", "enabledToast": "Proaktywne podpowiedzi Coacha włączone.", "disabledToast": "Proaktywne podpowiedzi Coacha wyłączone.", - "saveError": "Nie udało się zapisać. Spróbuj ponownie później." + "saveError": "Nie udało się zapisać. Spróbuj ponownie później.", + "suggestionsLabel": "Przykładowe sugestie", + "suggestionsDesc": "Czasami pokazuje gotowe przykładowe pytanie na ekranie Coacha.", + "suggestionsAria": "Przełącz przykładowe sugestie Coacha" }, "lowStock": { "title": "Alert niskiego zapasu", diff --git a/src/app/api/auth/me/notification-prefs/__tests__/route.test.ts b/src/app/api/auth/me/notification-prefs/__tests__/route.test.ts index 46dbd045d..77824fcd8 100644 --- a/src/app/api/auth/me/notification-prefs/__tests__/route.test.ts +++ b/src/app/api/auth/me/notification-prefs/__tests__/route.test.ts @@ -102,6 +102,7 @@ describe("GET /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -137,6 +138,7 @@ describe("GET /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -174,6 +176,7 @@ describe("GET /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -222,6 +225,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -246,6 +250,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -274,6 +279,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -294,6 +300,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -367,6 +374,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -405,6 +413,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -429,6 +438,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -472,6 +482,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, diff --git a/src/components/insights/coach-panel/coach-conversation.tsx b/src/components/insights/coach-panel/coach-conversation.tsx index 0432e6a77..1457ed0fc 100644 --- a/src/components/insights/coach-panel/coach-conversation.tsx +++ b/src/components/insights/coach-panel/coach-conversation.tsx @@ -40,6 +40,7 @@ import { MobileRailTray } from "./mobile-rail-tray"; import { SelfContextAdoptOffer } from "./self-context-adopt-offer"; import { SourcesRail } from "./sources-rail"; import { useResettableValue } from "./use-resettable-value"; +import { useCoachAmbientSuggestionsEnabled } from "@/hooks/use-coach-ambient-suggestions"; import { useCoachConversation, useCoachConversations, @@ -516,8 +517,15 @@ export function CoachConversation({ return false; } }); + // v1.25.0 — the per-user opt-out for proactive ambient suggestions gates the + // seeded opener (and skips its fetch when off). The A2 launch scope below is + // NOT ambient — it reflects how the chat was opened — so it stays unaffected. + const ambientSuggestionsEnabled = useCoachAmbientSuggestionsEnabled(); const seededEnabled = - heroActive && a2Metric === null && !seededDismissedToday; + heroActive && + a2Metric === null && + !seededDismissedToday && + ambientSuggestionsEnabled; const { data: seeded } = useQuery({ queryKey: queryKeys.coachSeededQuestion(), queryFn: async () => @@ -574,7 +582,11 @@ export function CoachConversation({ onSeed={seedComposer} /> ); - } else if (seeded?.signal && !seededDismissedToday) { + } else if ( + seeded?.signal && + !seededDismissedToday && + ambientSuggestionsEnabled + ) { // A3 — the notable derived signal. The label + opener are keyed on the // signal's sourceMetric (`readiness` / `recovery`); an unknown sentinel // (future detector additions) skips the opener rather than guessing. diff --git a/src/components/settings/coach-nudge-card.tsx b/src/components/settings/coach-nudge-card.tsx index 61e2fc8d1..9cc6705d1 100644 --- a/src/components/settings/coach-nudge-card.tsx +++ b/src/components/settings/coach-nudge-card.tsx @@ -33,6 +33,7 @@ interface CoachPrefsShape { nudgeVitals: boolean; nudgeRoutine: boolean; nudgeFrequency: "weekly" | "biweekly"; + ambientSuggestions: boolean; } interface NotificationPrefsShape { @@ -45,6 +46,7 @@ const COACH_PREF_DEFAULTS: CoachPrefsShape = { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }; const GROUP_FIELDS = [ @@ -254,6 +256,28 @@ export function CoachNudgeCard({ )} + {/* v1.25.0 — proactive ambient SUGGESTIONS opt-out. Independent of the + push-nudge master switch above (a user can keep nudges but silence + the example prompts), so it sits outside the `enabled` gate and rides + the same `coach` prefs blob. */} +
+
+

+ {t("notifications.coachNudge.suggestionsLabel")} +

+

+ {t("notifications.coachNudge.suggestionsDesc")} +

+
+ + patchCoach({ ambientSuggestions: next }, null) + } + disabled={!isAuthenticated || saving} + aria-label={t("notifications.coachNudge.suggestionsAria")} + /> +
{msg && (

+ apiGet("/api/auth/me/notification-prefs"), + enabled: isAuthenticated, + staleTime: 5 * 60 * 1000, + }); + return data?.coach?.ambientSuggestions ?? true; +} diff --git a/src/lib/validations/__tests__/notification-prefs.test.ts b/src/lib/validations/__tests__/notification-prefs.test.ts index 813f2c8ac..ee37ed340 100644 --- a/src/lib/validations/__tests__/notification-prefs.test.ts +++ b/src/lib/validations/__tests__/notification-prefs.test.ts @@ -109,6 +109,7 @@ describe("parseNotificationPrefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -132,6 +133,7 @@ describe("parseNotificationPrefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -157,6 +159,7 @@ describe("parseNotificationPrefs", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -198,6 +201,7 @@ describe("resolveNotificationPrefs (deep-merge)", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -227,6 +231,7 @@ describe("resolveNotificationPrefs (deep-merge)", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -253,6 +258,7 @@ describe("resolveNotificationPrefs (deep-merge)", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -279,6 +285,7 @@ describe("resolveNotificationPrefs (deep-merge)", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -306,6 +313,7 @@ describe("resolveNotificationPrefs (deep-merge)", () => { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, diff --git a/src/lib/validations/notification-prefs.ts b/src/lib/validations/notification-prefs.ts index 0f405c708..26abf53ab 100644 --- a/src/lib/validations/notification-prefs.ts +++ b/src/lib/validations/notification-prefs.ts @@ -125,6 +125,14 @@ const coachPrefsSchema = z nudgeRoutine: z.boolean(), /** v1.16.5 — frequency cap: one nudge per 7 or per 14 days. */ nudgeFrequency: z.enum(["weekly", "biweekly"]), + /** + * v1.25.0 — opt-out for the proactive ambient SUGGESTIONS surfaced in the + * UI (the daily seeded example opener on the Coach hero + the "try asking" + * prompt chips). Default ON. Separate from `nudgesEnabled` (the push-nudge + * master switch) so a user can keep the nudges but silence the example + * prompts, or vice versa. + */ + ambientSuggestions: z.boolean(), }) .partial(); @@ -200,6 +208,11 @@ export interface NotificationPrefs { nudgeRoutine: boolean; /** v1.16.5 — cap interval: "weekly" (7 d) or "biweekly" (14 d). */ nudgeFrequency: "weekly" | "biweekly"; + /** + * v1.25.0 — proactive ambient example suggestions in the UI (seeded + * opener + suggested-prompt chips). Default `true`. + */ + ambientSuggestions: boolean; }; measurementReminder: { /** @@ -256,6 +269,7 @@ export const DEFAULT_NOTIFICATION_PREFS: NotificationPrefs = { nudgeVitals: true, nudgeRoutine: true, nudgeFrequency: "weekly", + ambientSuggestions: true, }, measurementReminder: { clientManaged: false, @@ -447,6 +461,16 @@ export interface CoachNudgePrefs { minIntervalDays: number; } +/** + * v1.25.0 — the single source every proactive ambient SUGGESTION surface + * consults (the daily seeded example opener on the Coach hero + the + * suggested-prompt chips). Default `true`; a null / drifted row resolves to + * the documented default so the suggestions work out of the box. + */ +export function resolveCoachAmbientSuggestionsEnabled(raw: unknown): boolean { + return parseNotificationPrefs(raw).coach.ambientSuggestions; +} + export function resolveCoachNudgePrefs(raw: unknown): CoachNudgePrefs { const coach = parseNotificationPrefs(raw).coach; return {