mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
The regenerate button aborted its inline POST after a fixed 45 s, so on a slow local model every regenerate was cut while the server still had minutes left on the person's setting. It now uses clientAbortMs over the route's sequential model calls, never less per call than the briefing's own 180 s default, because the published value cannot say whether the person set it. A save of the about-me self-context derives the clarifying questions inline and ran under the 15 s request default, which reported a failed save while the server went on to commit it. All six save sites now take their abort from the same setting. Every other browser request that waits on a model either has no client timeout or only queues the work. Refs #1267
187 lines
6.7 KiB
TypeScript
187 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
import { useQueryClientMounted } from "@/hooks/_internal/use-query-client-safe";
|
|
import type {
|
|
AiCapabilityKey,
|
|
AiCapabilityState,
|
|
AiConsentState,
|
|
AiProviderState,
|
|
} from "@/lib/ai/capabilities/types";
|
|
import {
|
|
clientAbortMs,
|
|
PROVIDER_DEFAULT_TIMEOUT_MS,
|
|
} from "@/lib/ai/effective-timeout";
|
|
import type { CoachReasoningState } from "@/lib/ai/reasoning/resolve";
|
|
import type { AiUsageState } from "@/lib/ai/usage-level";
|
|
|
|
/**
|
|
* The web reader for the `ai` block on `GET /api/auth/me`.
|
|
*
|
|
* The server resolves every AI capability for the record this browser is
|
|
* inside, from every layer that can say no, and publishes the answer with the
|
|
* reason. This hook hands that answer to a surface and adds exactly one rule
|
|
* of its own: until the answer is known, a capability is unavailable. While
|
|
* `/me` is loading, when no query client is mounted (an isolated
|
|
* presentational render), or when the payload carries no well-formed block,
|
|
* the hook answers `check_failed`, so no AI surface paints and no model
|
|
* request fires on first render only to be refused. Failing open was how AI
|
|
* chrome used to flash and fire refusals before the switch set arrived.
|
|
*
|
|
* Never recompute a capability here or in a component; branch on
|
|
* `available`, and on `reason` only to say why.
|
|
*/
|
|
const UNKNOWN: AiCapabilityState = Object.freeze({
|
|
available: false,
|
|
reason: "check_failed",
|
|
onDeviceAllowed: false,
|
|
});
|
|
|
|
const NO_PROVIDER_STATE: AiProviderState = Object.freeze({
|
|
configured: false,
|
|
managedBy: null,
|
|
canConfigure: false,
|
|
responseTimeoutMs: PROVIDER_DEFAULT_TIMEOUT_MS,
|
|
});
|
|
|
|
/** One capability, for the record this browser is inside. */
|
|
export function useAiCapability(key: AiCapabilityKey): AiCapabilityState {
|
|
const hasClient = useQueryClientMounted();
|
|
if (!hasClient) return UNKNOWN;
|
|
// The branch is stable for the component's lifetime (a provider is either
|
|
// mounted around it or not), so the conditional hook call is safe.
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
return useAiCapabilityInner(key);
|
|
}
|
|
|
|
function useAiCapabilityInner(key: AiCapabilityKey): AiCapabilityState {
|
|
const { user } = useAuth();
|
|
return user?.ai?.capabilities?.[key] ?? UNKNOWN;
|
|
}
|
|
|
|
/** The account's provider state: configured, managed by whom, configurable here. */
|
|
export function useAiProviderState(): AiProviderState {
|
|
const hasClient = useQueryClientMounted();
|
|
if (!hasClient) return NO_PROVIDER_STATE;
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
return useAiProviderStateInner();
|
|
}
|
|
|
|
function useAiProviderStateInner(): AiProviderState {
|
|
const { user } = useAuth();
|
|
return user?.ai?.provider ?? NO_PROVIDER_STATE;
|
|
}
|
|
|
|
/**
|
|
* #1267 — the timeout for a save of `PUT /api/coach/about-me`. A save that
|
|
* changes the text or the shared sections waits on one model call (the
|
|
* clarifying questions) before it answers, so the fixed 15 s request default
|
|
* reported a failed save while a slow model was still within the person's
|
|
* response timeout, and the server went on to commit it.
|
|
*/
|
|
export function useAboutMeSaveTimeoutMs(): number {
|
|
const { responseTimeoutMs } = useAiProviderState();
|
|
return clientAbortMs(responseTimeoutMs, 1);
|
|
}
|
|
|
|
/**
|
|
* v1.41 — the Coach's thinking depth, resolved on the server (`coachReasoning`
|
|
* on `GET /api/auth/me`), or `null` while it is not known: `/me` loading, no
|
|
* query client, or a server that does not publish it. Render the settings
|
|
* from it; never recompute the cap or the switch.
|
|
*/
|
|
export function useCoachReasoning(): CoachReasoningState | null {
|
|
const hasClient = useQueryClientMounted();
|
|
if (!hasClient) return null;
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
return useCoachReasoningInner();
|
|
}
|
|
|
|
function useCoachReasoningInner(): CoachReasoningState | null {
|
|
const { user } = useAuth();
|
|
return user?.coachReasoning ?? null;
|
|
}
|
|
|
|
/**
|
|
* v1.43 (#1217) — the record's AI usage level (`ai.usage` on
|
|
* `GET /api/auth/me`): the level in effect, the person's choice, the
|
|
* operator's bounds, the three options with their estimates. `null` while it
|
|
* is not known or on a server that does not publish it; the setting is then
|
|
* not shown. Render from it; never compute an estimate or an allowance here.
|
|
*/
|
|
export function useAiUsage(): AiUsageState | null {
|
|
const hasClient = useQueryClientMounted();
|
|
if (!hasClient) return null;
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
return useAiUsageInner();
|
|
}
|
|
|
|
function useAiUsageInner(): AiUsageState | null {
|
|
const { user } = useAuth();
|
|
return user?.ai?.usage ?? null;
|
|
}
|
|
|
|
/**
|
|
* v1.43.2 — the standing AI consent (`ai.consent` on `GET /api/auth/me`):
|
|
* given (and since when), needed (and for which services), or not needed.
|
|
* `null` while it is not known; a consent surface then says nothing rather
|
|
* than misreport the person's decision. Render from it; never decide here
|
|
* whether consent is needed.
|
|
*/
|
|
export function useAiConsent(): AiConsentState | null {
|
|
const hasClient = useQueryClientMounted();
|
|
if (!hasClient) return null;
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
return useAiConsentInner();
|
|
}
|
|
|
|
function useAiConsentInner(): AiConsentState | null {
|
|
const { user } = useAuth();
|
|
return user?.ai?.consent ?? null;
|
|
}
|
|
|
|
/**
|
|
* The server's answer for one capability, or `null` while there is none yet
|
|
* (`/me` loading, no signed-in account, no query client). For the few places
|
|
* that act on an unavailable capability rather than just hiding: a Coach page
|
|
* that sends its visitor elsewhere must not do so on the loading frame, when
|
|
* `useAiCapability` already reads unavailable.
|
|
*/
|
|
export function useAiCapabilityAnswer(
|
|
key: AiCapabilityKey,
|
|
): AiCapabilityState | null {
|
|
const hasClient = useQueryClientMounted();
|
|
if (!hasClient) return null;
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
return useAiCapabilityAnswerInner(key);
|
|
}
|
|
|
|
function useAiCapabilityAnswerInner(
|
|
key: AiCapabilityKey,
|
|
): AiCapabilityState | null {
|
|
const { user, isLoading } = useAuth();
|
|
if (isLoading || !user) return null;
|
|
return user.ai?.capabilities?.[key] ?? UNKNOWN;
|
|
}
|
|
|
|
/**
|
|
* Every capability at once, or `null` until `/me` has answered. For the one
|
|
* surface that explains the whole set (the operator notice in Settings → AI);
|
|
* a surface that shows or hides one feature reads `useAiCapability`.
|
|
*/
|
|
export function useAiCapabilityMap(): Partial<
|
|
Record<AiCapabilityKey, AiCapabilityState>
|
|
> | null {
|
|
const hasClient = useQueryClientMounted();
|
|
if (!hasClient) return null;
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
return useAiCapabilityMapInner();
|
|
}
|
|
|
|
function useAiCapabilityMapInner(): Partial<
|
|
Record<AiCapabilityKey, AiCapabilityState>
|
|
> | null {
|
|
const { user } = useAuth();
|
|
return user?.ai?.capabilities ?? null;
|
|
}
|