mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
The dashboard snapshot fires the moment login succeeds instead of waiting for the page chunk (start ~670 to ~200 ms throttled). Guided follow-ups are personalised from the user's snapshot and an answer gets the coach's streamed reaction before the next question. Archived schedule eras can be corrected through a superseding revision that keeps the original as an audit trail, era writes serialise per medication, and duplicate dose times collapse in validation. A rolling medication whose next dose is tomorrow can no longer read as heavily overdue — the card status derives strictly from the served next-due — and the adherence bars keep a fixed geometry in every state. The server-wide assistant key gains its admin card, the one duplicated coach preference collapses onto a single owner, twenty-seven admin and settings card headers move onto the shared pattern with sixty containers normalised, and the remaining hydration warnings are gone.
172 lines
5.8 KiB
TypeScript
172 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { Sparkles } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import { queryKeys } from "@/lib/query-keys";
|
|
import { SettingsToggle } from "./_shared";
|
|
import { apiGet, apiPut } from "@/lib/api/api-fetch";
|
|
|
|
/**
|
|
* v1.4.31 — operator-side panel for the six assistant feature
|
|
* flags. The master toggle gates the whole assistant; five
|
|
* sub-toggles carve specific surfaces (Coach, Daily Briefing,
|
|
* per-metric status cards, correlation narration, Health-Score
|
|
* delta explainer).
|
|
*
|
|
* UX:
|
|
* - Master toggle at the top. When off, the sub-toggles are
|
|
* visually greyed out (kept rendered so the operator can see
|
|
* which sub-flags are individually flipped before they unmute
|
|
* the master) but cannot be flipped via the disabled `<Switch>`.
|
|
* - Optimistic flip + server confirm via `useMutation`; failure
|
|
* surfaces a toast and reverts the in-flight toggle by
|
|
* re-invalidating the read query.
|
|
*/
|
|
|
|
interface AssistantFlagsResponse {
|
|
raw: {
|
|
assistantEnabled: boolean;
|
|
assistantCoachEnabled: boolean;
|
|
assistantBriefingEnabled: boolean;
|
|
assistantInsightStatusEnabled: boolean;
|
|
assistantCorrelationsEnabled: boolean;
|
|
assistantHealthScoreExplainerEnabled: boolean;
|
|
};
|
|
resolved: {
|
|
enabled: boolean;
|
|
coach: boolean;
|
|
briefing: boolean;
|
|
insightStatus: boolean;
|
|
correlations: boolean;
|
|
healthScoreExplainer: boolean;
|
|
};
|
|
}
|
|
|
|
function useAssistantFlags() {
|
|
return useQuery({
|
|
queryKey: queryKeys.adminAssistantFlags(),
|
|
queryFn: async () => {
|
|
return apiGet<AssistantFlagsResponse>(
|
|
"/api/admin/settings/assistant-flags",
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
function useUpdateAssistantFlags() {
|
|
const client = useQueryClient();
|
|
const { t } = useTranslations();
|
|
return useMutation({
|
|
mutationFn: async (
|
|
patch: Partial<AssistantFlagsResponse["raw"]>,
|
|
): Promise<AssistantFlagsResponse> => {
|
|
return apiPut<AssistantFlagsResponse>(
|
|
"/api/admin/settings/assistant-flags",
|
|
patch,
|
|
);
|
|
},
|
|
onSuccess: (data) => {
|
|
client.setQueryData(queryKeys.adminAssistantFlags(), data);
|
|
// Bust the runtime `/api/feature-flags` cache so the operator
|
|
// sees the toggled surface react within the same session.
|
|
client.invalidateQueries({ queryKey: queryKeys.featureFlags() });
|
|
toast.success(t("common.saved"));
|
|
},
|
|
onError: (err) => {
|
|
toast.error(
|
|
err instanceof Error && err.message
|
|
? err.message
|
|
: t("admin.settingsSaveError"),
|
|
);
|
|
client.invalidateQueries({
|
|
queryKey: queryKeys.adminAssistantFlags(),
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function AssistantSection() {
|
|
const { t } = useTranslations();
|
|
const { data } = useAssistantFlags();
|
|
const mutation = useUpdateAssistantFlags();
|
|
|
|
const raw = data?.raw;
|
|
const masterOn = raw?.assistantEnabled ?? true;
|
|
const disabledSubs = !masterOn || mutation.isPending;
|
|
|
|
return (
|
|
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
|
<SettingsCardHeader
|
|
icon={Sparkles}
|
|
title={t("admin.assistant.title")}
|
|
description={t("admin.assistant.description")}
|
|
/>
|
|
<div className="mt-4 space-y-4 pl-7">
|
|
<SettingsToggle
|
|
label={t("admin.assistant.master.title")}
|
|
description={t("admin.assistant.master.description")}
|
|
checked={raw?.assistantEnabled ?? true}
|
|
onCheckedChange={(checked) =>
|
|
mutation.mutate({ assistantEnabled: checked })
|
|
}
|
|
disabled={mutation.isPending}
|
|
/>
|
|
|
|
<div className="border-border space-y-4 border-t pt-4">
|
|
<SettingsToggle
|
|
label={t("admin.assistant.coach.title")}
|
|
description={t("admin.assistant.coach.description")}
|
|
checked={raw?.assistantCoachEnabled ?? true}
|
|
onCheckedChange={(checked) =>
|
|
mutation.mutate({ assistantCoachEnabled: checked })
|
|
}
|
|
disabled={disabledSubs}
|
|
/>
|
|
<SettingsToggle
|
|
label={t("admin.assistant.briefing.title")}
|
|
description={t("admin.assistant.briefing.description")}
|
|
checked={raw?.assistantBriefingEnabled ?? true}
|
|
onCheckedChange={(checked) =>
|
|
mutation.mutate({ assistantBriefingEnabled: checked })
|
|
}
|
|
disabled={disabledSubs}
|
|
/>
|
|
<SettingsToggle
|
|
label={t("admin.assistant.insightStatus.title")}
|
|
description={t("admin.assistant.insightStatus.description")}
|
|
checked={raw?.assistantInsightStatusEnabled ?? true}
|
|
onCheckedChange={(checked) =>
|
|
mutation.mutate({ assistantInsightStatusEnabled: checked })
|
|
}
|
|
disabled={disabledSubs}
|
|
/>
|
|
<SettingsToggle
|
|
label={t("admin.assistant.correlations.title")}
|
|
description={t("admin.assistant.correlations.description")}
|
|
checked={raw?.assistantCorrelationsEnabled ?? true}
|
|
onCheckedChange={(checked) =>
|
|
mutation.mutate({ assistantCorrelationsEnabled: checked })
|
|
}
|
|
disabled={disabledSubs}
|
|
/>
|
|
<SettingsToggle
|
|
label={t("admin.assistant.healthScoreExplainer.title")}
|
|
description={t("admin.assistant.healthScoreExplainer.description")}
|
|
checked={raw?.assistantHealthScoreExplainerEnabled ?? true}
|
|
onCheckedChange={(checked) =>
|
|
mutation.mutate({
|
|
assistantHealthScoreExplainerEnabled: checked,
|
|
})
|
|
}
|
|
disabled={disabledSubs}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|