mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
A withheld table now leaves a line saying why (module off, or the stored tables could not be read) and a failed read shows an error row with a retry, so the Data used count matches what is listed. A table copied from an earlier answer names that answer's date under its title. The preview shows the latest twelve periods of a time series, and the truncation note says the latest rows are kept, which is what the server keeps. The title, copy and view controls sit above the scrolling table with the title kept as a hidden caption, and a reply with tables takes the full column width. The step headers and the related-metric chip read as label: value in all seven locales, so a metric name no longer sits capitalised mid-sentence.
78 lines
2.5 KiB
TypeScript
78 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* v1.39.4 — the stored tables of one Coach message, fetched lazily.
|
|
*
|
|
* A thread can hold many messages with tables, and each table set is
|
|
* decrypted on the server per request, so nothing is fetched until the
|
|
* message scrolls near the viewport: mount the returned `ref` on the
|
|
* element that shows the tables. `enabled` gates the whole read (a message
|
|
* whose provenance lists no tables never asks). Once fetched, the tables are
|
|
* the stored snapshot and do not change, so they stay fresh indefinitely.
|
|
*/
|
|
import { useEffect, useState, type RefCallback } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
|
|
import type { CoachResultEntry } from "@/lib/ai/coach/types";
|
|
import { apiGet } from "@/lib/api/api-fetch";
|
|
import { queryKeys } from "@/lib/query-keys";
|
|
|
|
export function useCoachMessageResults(args: {
|
|
conversationId: string | null;
|
|
messageId: string | null;
|
|
enabled: boolean;
|
|
}): {
|
|
ref: RefCallback<HTMLElement>;
|
|
results: CoachResultEntry[] | undefined;
|
|
isLoading: boolean;
|
|
isError: boolean;
|
|
/** Read again after a failure. */
|
|
refetch: () => void;
|
|
} {
|
|
const { conversationId, messageId, enabled } = args;
|
|
const [node, setNode] = useState<HTMLElement | null>(null);
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
// Without an observer (an old engine) there is nothing to wait for.
|
|
const canObserve = typeof IntersectionObserver !== "undefined";
|
|
|
|
useEffect(() => {
|
|
if (!enabled || visible || !node || !canObserve) return;
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries.some((entry) => entry.isIntersecting)) setVisible(true);
|
|
},
|
|
{ rootMargin: "200px" },
|
|
);
|
|
observer.observe(node);
|
|
return () => observer.disconnect();
|
|
}, [enabled, visible, node, canObserve]);
|
|
|
|
const query = useQuery({
|
|
queryKey: queryKeys.coachMessageResults(
|
|
conversationId ?? "",
|
|
messageId ?? "",
|
|
),
|
|
queryFn: async (): Promise<CoachResultEntry[]> => {
|
|
const data = await apiGet<{ results: CoachResultEntry[] }>(
|
|
`/api/insights/chat/${encodeURIComponent(conversationId ?? "")}/messages/${encodeURIComponent(messageId ?? "")}/results`,
|
|
);
|
|
return data.results;
|
|
},
|
|
enabled:
|
|
enabled &&
|
|
(visible || !canObserve) &&
|
|
conversationId !== null &&
|
|
messageId !== null,
|
|
staleTime: Infinity,
|
|
});
|
|
|
|
return {
|
|
ref: setNode,
|
|
results: query.data,
|
|
isLoading: query.isLoading,
|
|
isError: query.isError,
|
|
refetch: () => void query.refetch(),
|
|
};
|
|
}
|