"use client"; /** * v1.11.2 — Settings → AI "What the Coach remembers" panel. * * Surfaces the durable `CoachFact` rows the assistant has stored about * the user (the v1.11.1 routes already ship the data), grouped by * category, each with a relative "learned " stamp and a "forget" * control. A bulk "forget everything" action clears the lot. * * GET /api/insights/coach/facts → { data: { facts: [...] } } * DELETE /api/insights/coach/facts/{id} → { data: { deleted } } * DELETE /api/insights/coach/facts → { data: { cleared } } * * Reads unwrap `(await res.json()).data` per the envelope convention; * every read/write routes its key through `queryKeys.coachFacts()` so * a forget invalidates the list. * * v1.41 — the Coach's one memory. Each entry says where it came from * ("from you" for what the person asked it to keep, "from the Coach" for what * it kept or worked out itself), and can be edited in place * (`PATCH /api/insights/coach/facts/{id}`) as well as forgotten. The Coach's * quick settings link here (`#coach-memory`). * * v1.39 — never gated on the Coach. What the Coach stored is the person's * own record: it stays readable and deletable while the Coach is off for any * reason (the operator's switch, Hide Coach, no provider, no consent). The * facts routes are data routes and ask no AI gate. The same holds for the * stored reminders (``). Settings → Coach always * shows this card; while that page is not reachable, Settings → AI shows it * through `` below, whenever rows exist. */ import { useMemo, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Brain, Loader2, Pencil, Trash2 } from "lucide-react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { QueryErrorRow } from "@/components/ui/query-error-row"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { ConfirmButton } from "@/components/ui/confirm-button"; import { EmptyState } from "@/components/ui/empty-state"; import { formatDateOrRelative } from "@/lib/format"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { toast } from "sonner"; import { showErrorToast } from "@/lib/errors/error-toast"; import { apiDelete } from "@/lib/api/api-fetch"; import { Textarea } from "@/components/ui/textarea"; import { COACH_MEMORY_LIST_KEYS, REMEMBER_FACT_MAX_CHARS, } from "@/lib/ai/coach/memory/shared"; import { useCoachFacts, useEditCoachFact, useForgetCoachFact, type CoachFactDTO, } from "@/components/insights/coach-panel/use-coach-facts"; import { CoachConversationsMemoryCard } from "@/components/settings/coach-conversations-memory-card"; import { CoachRemindersSection } from "@/components/settings/coach-reminders-section"; import { CoachPlansMemoryCard } from "@/components/settings/coach-plans-memory-card"; /** * The categories in the order the list shows them: goals first, then how the * person likes things, then health, then the rest. */ const FACT_CATEGORIES = [ "goal", "preference", "condition", "medication", "constraint", "context", ] as const; type FactCategory = (typeof FACT_CATEGORIES)[number]; const CATEGORY_LABEL_KEY: Record = { goal: "settings.ai.coachMemory.categoryGoal", preference: "settings.ai.coachMemory.categoryPreference", condition: "settings.ai.coachMemory.categoryCondition", medication: COACH_MEMORY_LIST_KEYS.categoryMedication, constraint: "settings.ai.coachMemory.categoryConstraint", context: "settings.ai.coachMemory.categoryContext", }; /** "from you" for what the person asked to keep; everything else the Coach. */ export function factSourceKey( fact: Pick, ): string | null { if (!fact.source) return null; return fact.source === "user" ? COACH_MEMORY_LIST_KEYS.sourceUser : COACH_MEMORY_LIST_KEYS.sourceCoach; } /** One entry, read or being edited. */ function FactRow({ fact, isAuthenticated, forgetting, onForget, }: { fact: CoachFactDTO; isAuthenticated: boolean; forgetting: boolean; onForget: () => void; }) { const { t } = useTranslations(); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(fact.text); const edit = useEditCoachFact({ // The edited words in place are the confirmation. onSuccess: () => setEditing(false), onError: () => showErrorToast(t("admin.settingsSaveError")), }); const sourceKey = factSourceKey(fact); const trimmed = draft.trim(); const inputId = `coach-memory-edit-${fact.id}`; return (
  • {editing ? (
    { e.preventDefault(); if (trimmed && trimmed !== fact.text) { edit.mutate({ id: fact.id, fact: trimmed }); } else { setEditing(false); } }} >