"use client"; /** * v1.15.20 — Settings → AI "About me" panel (free text only). * v1.16.0 — structured self-context: alongside the free text the user * notes allergies / intolerances. All fields are encrypted at rest; * age/gender stay on the profile and are merged into the prompt * server-side — the panel says so instead of asking again. * * v1.25.12 — the pre-existing / chronic conditions and the "what should the * Coach watch" focus moved out of this panel into Settings → Anamnese, where * they sit with allergies + family history as one medical history. Editing them * has a single home there now; this panel keeps the free-text note and the * allergies line. The store is unchanged — every field is still part of the * `/api/coach/about-me` payload; the PUT here simply omits the two conditions * fields, which are preserved server-side. * * GET /api/coach/about-me — full structured payload + pending questions * PUT /api/coach/about-me — writes aboutMe (empty string clears) + allergies; * conditions / coachFocus are omitted here and left untouched * * After a save the server derives up to 3 clarifying questions (AI * when a provider + budget allow, deterministic hints otherwise); the * panel surfaces them in a quiet callout and the Coach composer * renders them as tappable chips. * * Plain text only — every value renders exclusively through inputs and * React text children; no markdown anywhere. */ import { useState } from "react"; import Link from "next/link"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, MessageCircleQuestion, NotebookPen, Trash2, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; 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 { cn } from "@/lib/utils"; import { apiGet, apiPut } from "@/lib/api/api-fetch"; const FALLBACK_MAX_CHARS = 4000; const FALLBACK_FIELD_MAX_CHARS = 500; interface AboutMeData { aboutMe: string | null; conditions: string | null; allergies: string | null; coachFocus: string | null; pendingQuestions: string[]; updatedAt: string | null; maxChars: number; fieldMaxChars: number; } type StructuredKey = "allergies"; async function fetchAboutMe(): Promise { return apiGet("/api/coach/about-me"); } const FIELD_CLASSES = "border-input bg-background placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-1 focus-visible:outline-none"; export function AboutMeSection({ isAuthenticated, }: { isAuthenticated: boolean; }) { const { t } = useTranslations(); const queryClient = useQueryClient(); const [drafts, setDrafts] = useState<{ aboutMe: string | null; allergies: string | null; }>({ aboutMe: null, allergies: null }); const query = useQuery({ queryKey: queryKeys.coachAboutMe(), queryFn: fetchAboutMe, enabled: isAuthenticated, }); const saved = { aboutMe: query.data?.aboutMe ?? "", allergies: query.data?.allergies ?? "", }; const maxChars = query.data?.maxChars ?? FALLBACK_MAX_CHARS; const fieldMaxChars = query.data?.fieldMaxChars ?? FALLBACK_FIELD_MAX_CHARS; const pendingQuestions = query.data?.pendingQuestions ?? []; const value = { aboutMe: drafts.aboutMe ?? saved.aboutMe, allergies: drafts.allergies ?? saved.allergies, }; const dirty = value.aboutMe !== saved.aboutMe || value.allergies !== saved.allergies; const hasAnyContent = saved.aboutMe.length > 0 || saved.allergies.length > 0; // Quiet completeness meter: one segment per answered field. Reads // the SAVED state (not the draft) so it only moves on persistence. const answered = [saved.allergies, saved.aboutMe].filter( (v) => v.length > 0, ).length; const save = useMutation({ mutationKey: queryKeys.coachAboutMe(), // Send only the two fields this panel owns. `conditions` / `coachFocus` // are omitted so the server preserves them (they are edited under Anamnese). mutationFn: async (input: { aboutMe: string; allergies: string }) => { return apiPut("/api/coach/about-me", input); }, onSuccess: (next) => { const cleared = !next.aboutMe && !next.allergies; toast.success( cleared ? t("settings.ai.aboutMe.clearedToast") : t("settings.ai.aboutMe.savedToast"), ); setDrafts({ aboutMe: null, allergies: null }); queryClient.invalidateQueries({ queryKey: queryKeys.coachAboutMe() }); queryClient.invalidateQueries({ queryKey: queryKeys.coachAboutMeQuestions(), }); }, onError: () => { toast.error(t("settings.ai.aboutMe.saveError")); }, }); const disabled = !isAuthenticated || query.isLoading || save.isPending; function structuredField( key: StructuredKey, label: string, placeholder: string, ) { return (