Files
HealthLog/src/hooks/use-coach-plans.ts
T
Marc-André Bombeck 11bafd68b5 feat(coach): one quiet status line, reply pills and memory notes in the thread
The top of an answer is one line that says what the Coach is doing right
now (thinking, fetching a metric, summarising the readings, writing),
live from the activity frames, with the seconds so far. It never opens
by itself: the trail stays closed while the turn runs, when it asks,
when it stops early, after the answer and after a reload, and opens
only on a tap. Once the answer is in, the line settles into one summary
of lookups and seconds. A table read during the turn shows as a
one-line preview with a sparkline until the answer takes it over.

Clarifying choices, follow-ups and the answers to a fact or plan
proposal share one pattern: quiet pills in the answer's column whose
tap sends the label as the person's own message. The clarification
card and the in-thread plan card are gone. A fact the Coach kept is one
meta line with Undo, an assumption one line with Change. The remember
button on a message now keeps it in the Coach's memory.
2026-10-06 20:47:17 +02:00

100 lines
3.1 KiB
TypeScript

"use client";
/**
* v1.27.x — read + mutate the user's Coach goal / if-then plans.
*
* One shared hook for the chat thread's proposal confirm cards and the
* `/coach/plans` management page. Reads unwrap `(await res.json()).data` per
* the envelope convention; every key routes through `queryKeys.coachPlans()`
* so a lifecycle mutation invalidates both list slots at once. Mirrors
* `use-coach-reminders.ts`, the sibling episodic-memory hook.
*
* The extractor is the only writer of plan TEXT — this surface only confirms
* (proposed → active), marks met / abandoned, or soft-deletes. There is no
* prose editor by design: the PATCH contract never accepts the encrypted
* free-text fields.
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query-keys";
import { apiDelete, apiGet, apiPatch } from "@/lib/api/api-fetch";
export interface CoachPlanDTO {
id: string;
metric: string;
/** Null only when the row's encryption key id rotated out of the map. */
ifCue: string | null;
thenAction: string | null;
target: string | null;
status: string;
reviewDate: string | null;
sourceConversationId: string | null;
createdAt: string;
updatedAt: string;
}
interface PlanFilter {
/** Single lifecycle status (`proposed`, `active`, …). */
status?: string;
/** Named group: `open` | `past` | `all`. Mutually exclusive with status. */
scope?: string;
}
function filterKey(filter?: PlanFilter): string | undefined {
if (filter?.status) return `status:${filter.status}`;
if (filter?.scope) return `scope:${filter.scope}`;
return undefined;
}
async function fetchPlans(filter?: PlanFilter): Promise<CoachPlanDTO[]> {
const qs = filter?.status
? `?status=${encodeURIComponent(filter.status)}`
: filter?.scope
? `?scope=${encodeURIComponent(filter.scope)}`
: "";
const data = await apiGet<{ plans?: CoachPlanDTO[] } | undefined>(
`/api/coach/plans${qs}`,
);
return data?.plans ?? [];
}
/** List the caller's plans, optionally filtered by status or scope. */
export function useCoachPlans(opts?: {
filter?: PlanFilter;
enabled?: boolean;
}) {
const filter = opts?.filter;
return useQuery({
queryKey: queryKeys.coachPlans(filterKey(filter)),
queryFn: () => fetchPlans(filter),
enabled: opts?.enabled ?? true,
});
}
/** Lifecycle + delete mutations, invalidating every plans list slot. */
export function useCoachPlanMutations() {
const queryClient = useQueryClient();
const invalidate = () =>
queryClient.invalidateQueries({ queryKey: queryKeys.coachPlansAll() });
const setStatus = useMutation({
mutationFn: async (args: { id: string; status: string }) => {
await apiPatch(`/api/coach/plans/${encodeURIComponent(args.id)}`, {
status: args.status,
});
return args.id;
},
onSuccess: invalidate,
});
const remove = useMutation({
mutationFn: async (id: string) => {
await apiDelete(`/api/coach/plans/${encodeURIComponent(id)}`);
return id;
},
onSuccess: invalidate,
});
return { setStatus, remove };
}