mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
feat(labs): add an AI assessment and stat strip to the biomarker detail page
Give each biomarker a detail page that matches the metric sub-pages: a numbers-first Min/Max/Median/Mean strip over the numeric readings and a data-grounded AI assessment card. The assessment is served by a new read-only endpoint GET /api/insights/biomarker-assessment?biomarkerId=&locale=, returning the same envelope as /api/insights/metric-status so InsightStatusCard consumes it unchanged. A new generator reads the marker's LabResult history and reuses the shared provider runner, the stale-while-revalidate read-only path, and the persist helper. Regeneration is gated on a fingerprint of the latest reading, so the assessment is rebuilt only when a new reading lands; an idle marker re-stamps the cached text without an LLM call. The biomarker: scope rides the existing on-demand status queue. The stat strip and the assessment card both self-gate on numeric data, so a qualitative-only or brand-new marker paints neither.
This commit is contained in:
1 parent
02d527720a
commit
0790a96ca4
10 files changed
+779
-2
No files matched your search
@@ -241,3 +241,57 @@ export function useInsightMetricStatus(
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-biomarker assessment loader for the lab-marker detail page. Mirrors
|
||||
* `useInsightMetricStatus` exactly — the same `InsightStatusData` envelope,
|
||||
* the 8 s client ceiling, the no-retry policy, and the bounded `preparing`
|
||||
* poll — but keyed by the marker id against
|
||||
* `GET /api/insights/biomarker-assessment?biomarkerId=…`.
|
||||
*
|
||||
* Read-only + stale-while-revalidate: the route serves cached text and warms
|
||||
* a regeneration out of band on a cache miss, exactly like every metric
|
||||
* page. Generation never warms on mount beyond that shared seam; the worker
|
||||
* cron + the read-only enqueue are the only producers.
|
||||
*
|
||||
* `enabled` lets the page suppress the fetch when the marker has no readings
|
||||
* (the page renders its empty state instead of the card), so a brand-new
|
||||
* marker never fires an assessment round-trip.
|
||||
*/
|
||||
export function useInsightBiomarkerAssessment(
|
||||
biomarkerId: string,
|
||||
enabled = true,
|
||||
) {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { locale } = useTranslations();
|
||||
|
||||
return useQuery({
|
||||
queryKey: queryKeys.insightsBiomarkerAssessment(biomarkerId, locale),
|
||||
queryFn: async (): Promise<InsightStatusData> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutHandle = setTimeout(
|
||||
() => controller.abort(),
|
||||
STATUS_TIMEOUT_MS,
|
||||
);
|
||||
try {
|
||||
return await apiGet<InsightStatusData>(
|
||||
`/api/insights/biomarker-assessment?biomarkerId=${encodeURIComponent(
|
||||
biomarkerId,
|
||||
)}&locale=${locale}`,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
} finally {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
},
|
||||
enabled: isAuthenticated && enabled && biomarkerId.length > 0,
|
||||
staleTime: 60 * 1000,
|
||||
retry: 0,
|
||||
refetchInterval: (query) =>
|
||||
nextStatusPollInterval(
|
||||
query.state.data?.preparing,
|
||||
query.state.dataUpdateCount,
|
||||
query.state.data?.revalidating,
|
||||
),
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user