mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
feat(coach): add an opt-out for proactive ambient suggestions
Add a per-user toggle that turns off the proactive ambient example suggestions (the daily seeded opener on the Coach hero). The flag rides the existing coach notification-prefs blob (coach.ambientSuggestions, default on) — no schema change — and a shared hook makes it the single source every ambient suggestion surface consults, so the seeded opener and its fetch both stand down when off.
This commit is contained in:
1 parent
e55058a1ce
commit
c862b4880f
12 files changed
+139
-8
No files matched your search
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 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. */}
|
||||
<div className="border-border/60 mt-4 flex min-h-11 items-center justify-between gap-3 border-t pt-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm">
|
||||
{t("notifications.coachNudge.suggestionsLabel")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("notifications.coachNudge.suggestionsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={resolved.ambientSuggestions}
|
||||
onCheckedChange={(next) =>
|
||||
patchCoach({ ambientSuggestions: next }, null)
|
||||
}
|
||||
disabled={!isAuthenticated || saving}
|
||||
aria-label={t("notifications.coachNudge.suggestionsAria")}
|
||||
/>
|
||||
</div>
|
||||
{msg && (
|
||||
<p
|
||||
role="status"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
|
||||
/**
|
||||
* v1.25.0 — the single client-side gate for proactive ambient Coach
|
||||
* SUGGESTIONS (today: the daily seeded example opener on the Coach hero).
|
||||
*
|
||||
* Reads the per-user opt-out from the shared notification-prefs blob
|
||||
* (`coach.ambientSuggestions`). Default ON: an unauthenticated read, a
|
||||
* still-loading query, or a missing field all resolve to `true` so the
|
||||
* suggestions show out of the box and only hide once the user explicitly turns
|
||||
* them off in Settings. Shares the `authNotificationPrefs` query key with the
|
||||
* settings card, so toggling the switch there flips this read in lockstep.
|
||||
*/
|
||||
interface AmbientSuggestionsPrefsShape {
|
||||
coach?: { ambientSuggestions?: boolean };
|
||||
}
|
||||
|
||||
export function useCoachAmbientSuggestionsEnabled(): boolean {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { data } = useQuery({
|
||||
queryKey: queryKeys.authNotificationPrefs(),
|
||||
queryFn: async () =>
|
||||
apiGet<AmbientSuggestionsPrefsShape>("/api/auth/me/notification-prefs"),
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
return data?.coach?.ambientSuggestions ?? true;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user