mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
feat(settings): explain operator switches and keep the Coach memory reachable
Settings, AI says which AI features the server's operator switched off and shows the one setup hint while no provider is configured. The consent withdrawal says what it deletes and what stays, and a document-reading consent on file gets its own withdrawal. Regenerate follows the briefing capability and the connection test names an operator refusal. The Coach memory (facts and stored conversations) is never gated on the Coach: Settings, Coach always shows it, and Settings, AI shows it while the Coach page is out of reach, whenever rows exist. Onboarding offers AI only where it can be set up here: the done-screen panel and the getting-started row go away while the operator has AI off or inside somebody else's record.
This commit is contained in:
1 parent
4449b34ac7
commit
3d85445e50
24 files changed
+913
-232
No files matched your search
@@ -56,6 +56,7 @@ function inputs(
|
||||
dataSourceConnected: false,
|
||||
notificationsConfigured: false,
|
||||
insightsConfigured: false,
|
||||
aiConfigurable: true,
|
||||
dismissedIds: new Set<ChecklistItemId>(),
|
||||
upcomingVisitCount: 0,
|
||||
managedProfileCount: 0,
|
||||
@@ -94,13 +95,22 @@ describe("buildChecklist", () => {
|
||||
it("flags insights done once any provider can serve the user", () => {
|
||||
const off = buildChecklist(inputs({ insightsConfigured: false }));
|
||||
expect(off.find((i) => i.id === "insights")?.done).toBe(false);
|
||||
// `insightsConfigured` is derived from aiAvailable, which is true for a
|
||||
// personal key, a local model, an OAuth sign-in, OR the operator's
|
||||
// shared key — any one flips the row done.
|
||||
// `insightsConfigured` is `ai.provider.configured` from `/api/auth/me`,
|
||||
// true for a personal key, a local model, an OAuth sign-in, OR the
|
||||
// operator's shared key; any one flips the row done.
|
||||
const on = buildChecklist(inputs({ insightsConfigured: true }));
|
||||
expect(on.find((i) => i.id === "insights")?.done).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves the AI row out when AI cannot be set up here", () => {
|
||||
// `provider.canConfigure` is false while the operator has AI switched
|
||||
// off and inside a record somebody else owns. HealthLog is complete
|
||||
// without AI, so the list never carries a to-do nobody here can finish.
|
||||
const items = buildChecklist(inputs({ aiConfigurable: false }));
|
||||
expect(items.map((i) => i.id)).not.toContain("insights");
|
||||
expect(items).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("marks profile done when all three fields set", () => {
|
||||
const items = buildChecklist(inputs());
|
||||
expect(items.find((i) => i.id === "profile")?.done).toBe(true);
|
||||
@@ -293,6 +303,7 @@ describe("visibleChecklist + checklistProgress", () => {
|
||||
dataSourceConnected: false,
|
||||
notificationsConfigured: false,
|
||||
insightsConfigured: false,
|
||||
aiConfigurable: true,
|
||||
dismissedIds: new Set(),
|
||||
upcomingVisitCount: 0,
|
||||
managedProfileCount: 0,
|
||||
|
||||
@@ -74,12 +74,20 @@ export interface ChecklistInputs {
|
||||
/**
|
||||
* True iff any AI provider can serve this user — a personal key,
|
||||
* a local model, an OAuth sign-in, OR the operator's shared key.
|
||||
* Derived from `/api/user/ai-provider`'s `aiAvailable`, so the row
|
||||
* self-satisfies the moment insights become reachable (including on a
|
||||
* `ai.provider.configured` from `GET /api/auth/me`, so the row
|
||||
* self-satisfies the moment a provider is set up (including on a
|
||||
* deployment that ships a shared operator key). Presence-only — it
|
||||
* never decrypts a credential or probes liveness.
|
||||
*/
|
||||
insightsConfigured: boolean;
|
||||
/**
|
||||
* Whether AI can be set up from here at all (`ai.provider.canConfigure`):
|
||||
* false while the operator has AI switched off and inside a record
|
||||
* somebody else owns. The AI row exists only when true. HealthLog is
|
||||
* complete without AI, so the list never carries a to-do nobody on this
|
||||
* screen can finish.
|
||||
*/
|
||||
aiConfigurable: boolean;
|
||||
/** Dismissed item ids (per-item localStorage state). */
|
||||
dismissedIds: ReadonlySet<ChecklistItemId>;
|
||||
/**
|
||||
@@ -197,12 +205,16 @@ export function buildChecklist(inputs: ChecklistInputs): ChecklistItem[] {
|
||||
href: "/settings/notifications",
|
||||
dismissed: inputs.dismissedIds.has("notifications"),
|
||||
},
|
||||
{
|
||||
id: "insights",
|
||||
done: inputs.insightsConfigured,
|
||||
href: "/settings/ai",
|
||||
dismissed: inputs.dismissedIds.has("insights"),
|
||||
},
|
||||
...(inputs.aiConfigurable
|
||||
? [
|
||||
{
|
||||
id: "insights" as const,
|
||||
done: inputs.insightsConfigured,
|
||||
href: "/settings/ai",
|
||||
dismissed: inputs.dismissedIds.has("insights"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
if (!inputs.onboarding) return items;
|
||||
// The visit row exists only for the answer that asked for it; every other
|
||||
|
||||
Reference in new issue
Block a user