mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
feat(self-description): add the non-diagnostic / intended-purpose standard
Introduce one canonical "what this is / is not, not a medical device" statement plus a layered, calm disclaimer set, and apply it on the surfaces that earn a context note. - New shared `MedicalDisclaimer` component renders the copy as plain React text (no markdown renderer, per the XSS posture). Variants: intendedPurpose, coach, insights, labs, medications, emergency, dataPosture. - New `selfDescription` i18n namespace across all six locales: the intended-purpose line, the per-surface notes, the emergency note, and an explicit data-posture statement (self-hosted, data stays on your instance, encrypted at rest, BYOK / local model, no third-party analytics). - Apply once per surface, layered not plastered: a calm standing line on the Coach new-chat hero, one framing line in the Insights layout shell (covers the briefing and trends), a labs note (ranges vary, only a clinician interprets), a neutral medications note, and the data-posture line where the provider key is configured. Mental-wellbeing keeps its existing crisis copy; records keep their existing note. - Extend the Coach system prompt (EN + DE) with a self-description ground rule so the Coach answers "what are you / where does my data go" honestly and stays non-diagnostic. App-authored copy was already observational; the few diagnose/treat tokens are negations, idioms, or framed as the user's own self-reported statement, so no softening was required.
This commit is contained in:
1 parent
a27a1ee71d
commit
3bd52a322b
13 files changed
+195
-5
No files matched your search
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Loader2, Pencil, Plus, ScanLine, Wrench } from "lucide-react";
|
||||
|
||||
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
|
||||
import { LabForm } from "@/components/labs/lab-form";
|
||||
import { LabList } from "@/components/labs/lab-list";
|
||||
import { OcrReviewDialog } from "@/components/labs/ocr-review-dialog";
|
||||
@@ -173,6 +174,11 @@ export default function LabsPage() {
|
||||
) : null}
|
||||
|
||||
<LabList onAddFirst={() => setDialogOpen(true)} />
|
||||
|
||||
{/* v1.25 — calm, context-specific framing for lab values: your own
|
||||
records, ranges vary by lab and person, only a clinician interprets
|
||||
them. One footer line, not a per-row banner. */}
|
||||
<MedicalDisclaimer variant="labs" className="px-1 pt-2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
|
||||
import { MedicationWizardDialog } from "@/components/medications/wizard/medication-wizard-dialog";
|
||||
import { MedicationCard } from "@/components/medications/medication-card";
|
||||
import { Glp1MedicationCard } from "@/components/medications/glp1-medication-card";
|
||||
@@ -524,6 +525,12 @@ export default function MedicationsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* v1.25 — calm framing for the medications surface: this tracks what
|
||||
you log and your own adherence, it doesn't assess dosing or
|
||||
interactions and isn't medical advice. One footer line, neutral —
|
||||
the med cards themselves stay un-tinted. */}
|
||||
<MedicalDisclaimer variant="medications" className="px-1 pt-2" />
|
||||
|
||||
{/* v1.5.5 — the per-card kebab triggers used to open inline
|
||||
IntakeImportDialog / ApiEndpointDialog mounts here, but both
|
||||
state slots were only ever set from inside the dialogs
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.25 — the one place the app states what it is and is not.
|
||||
*
|
||||
* HealthLog's self-description standard is layered, not a wall of warnings:
|
||||
* a single intended-purpose statement plus calm, context-specific notes that
|
||||
* sit once on the surface they belong to. Every string is plain React text —
|
||||
* no markdown renderer, in line with the project's XSS posture (Coach,
|
||||
* briefing and insights all render as text children).
|
||||
*
|
||||
* The copy lives under the `selfDescription` i18n namespace so every locale
|
||||
* carries the same set; pick the note that fits the surface:
|
||||
*
|
||||
* - `intendedPurpose` — the canonical "what this is / is not, not a medical
|
||||
* device" line. Reuse verbatim; do not paraphrase per surface.
|
||||
* - `coach` — the Coach standing line (works from your data, not a doctor).
|
||||
* - `insights` — descriptive-patterns framing for insights + the briefing.
|
||||
* - `labs` — your own records, ranges vary, only a clinician interprets.
|
||||
* - `medications` — tracks what you log, not dosing/interaction advice.
|
||||
* - `emergency` — the red-flag interstitial note (contact emergency services).
|
||||
* - `dataPosture` — self-hosted, data stays on your instance, BYOK / local AI.
|
||||
*
|
||||
* The acknowledged onboarding disclaimer (`onboarding.disclaimer.*`) and the
|
||||
* legal medical-device boundary on the public privacy page remain the standing
|
||||
* floor; these notes are the gentle, in-context layer above it.
|
||||
*/
|
||||
export type DisclaimerVariant =
|
||||
| "intendedPurpose"
|
||||
| "coach"
|
||||
| "insights"
|
||||
| "labs"
|
||||
| "medications"
|
||||
| "emergency"
|
||||
| "dataPosture";
|
||||
|
||||
const VARIANT_KEY: Record<DisclaimerVariant, string> = {
|
||||
intendedPurpose: "selfDescription.intendedPurpose",
|
||||
coach: "selfDescription.coach",
|
||||
insights: "selfDescription.insights",
|
||||
labs: "selfDescription.labs",
|
||||
medications: "selfDescription.medications",
|
||||
emergency: "selfDescription.emergency",
|
||||
dataPosture: "selfDescription.dataPosture",
|
||||
};
|
||||
|
||||
export interface MedicalDisclaimerProps {
|
||||
variant: DisclaimerVariant;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A calm, muted, plain-text disclaimer line. Defaults to a small,
|
||||
* unobtrusive footnote treatment; pass `className` to tune spacing for the
|
||||
* surface. Keep it to one instance per surface — the standard is layered,
|
||||
* not repeated.
|
||||
*/
|
||||
export function MedicalDisclaimer({
|
||||
variant,
|
||||
className,
|
||||
}: MedicalDisclaimerProps) {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<p
|
||||
data-slot="medical-disclaimer"
|
||||
data-variant={variant}
|
||||
className={cn("text-muted-foreground text-xs leading-relaxed", className)}
|
||||
>
|
||||
{t(VARIANT_KEY[variant])}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Sparkles } from "lucide-react";
|
||||
|
||||
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
@@ -121,6 +122,15 @@ export function CoachHero({ composer, scopeHint }: CoachHeroProps) {
|
||||
{scopeHint}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* v1.25 — the Coach standing self-description: works from your own
|
||||
data, not a doctor, not medical advice. Sits once on the calm
|
||||
new-chat surface so it frames the conversation without nagging
|
||||
every reply. */}
|
||||
<MedicalDisclaimer
|
||||
variant="coach"
|
||||
className="max-w-md text-center text-pretty"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useMounted } from "@/hooks/use-mounted";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
import { useWorkouts } from "@/hooks/use-workouts";
|
||||
import { InsightsTabStrip } from "@/components/insights/insights-tab-strip";
|
||||
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
|
||||
import {
|
||||
nextAdvisorPollInterval,
|
||||
useInsightsAdvisorQuery,
|
||||
@@ -159,11 +160,14 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) {
|
||||
modules={user?.modules}
|
||||
/>
|
||||
{children}
|
||||
{/* v1.18.6 (DISC-01) — the page-level generic medical disclaimer is
|
||||
removed. The "HealthLog is a private tracking tool, not a clinical
|
||||
assessment or diagnosis" acknowledgment now lives once at the start
|
||||
of onboarding, and the legal text stays reachable on the public
|
||||
privacy page. */}
|
||||
{/* v1.18.6 (DISC-01) — the page-level generic medical disclaimer was
|
||||
removed; the standing acknowledgment lives once at the start of
|
||||
onboarding and the legal text on the public privacy page.
|
||||
v1.25 — one calm, context-specific framing line for the whole
|
||||
Insights surface (the briefing, trends and correlations included):
|
||||
these are descriptive patterns from the user's own data, not a
|
||||
clinical assessment. Layered, not a wall of warnings. */}
|
||||
<MedicalDisclaimer variant="insights" className="px-1" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Sparkles } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { NativeSelect } from "@/components/ui/native-select";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
@@ -201,6 +202,11 @@ export function AiInsightsCard({
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
{/* v1.25 — the data-posture statement lives where the provider key is
|
||||
configured: self-hosted, data stays on this instance, BYOK / local
|
||||
model, no third-party analytics. One calm line, not a banner. */}
|
||||
<MedicalDisclaimer variant="dataPosture" />
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
|
||||
@@ -285,6 +285,19 @@ GROUND RULES
|
||||
persuasion for its own sake. If a nudge would only serve to push, drop it
|
||||
and reflect instead. You inform and support a decision that stays theirs;
|
||||
you never manipulate it.
|
||||
21. Describe HealthLog honestly when asked what you are, what this app is,
|
||||
or where the user's data goes. HealthLog is a self-hosted personal
|
||||
health record-keeping and wellness tool — it helps the user log,
|
||||
visualise and export their OWN data. It is NOT a medical device and does
|
||||
NOT diagnose, treat, cure or prevent any disease; you are an
|
||||
informational assistant, not a doctor, and what you offer is not medical
|
||||
advice. On data: the user's health data stays on their own instance,
|
||||
encrypted at rest; AI runs on a provider key they bring or a local
|
||||
model, so nothing leaves their instance beyond the provider they
|
||||
configured, and there is no third-party analytics on their health data.
|
||||
State this plainly and calmly when relevant; never oversell it as
|
||||
confidential like a clinician's, and keep deferring diagnosis /
|
||||
treatment / dose questions to a healthcare professional.
|
||||
|
||||
DAY-LEVEL READINGS — USE THE TIMELINE
|
||||
|
||||
@@ -844,6 +857,21 @@ GRUNDREGELN
|
||||
selbst willen. Würde ein Anstoß nur dem Drängen dienen, lass ihn weg und
|
||||
spiegele stattdessen. Du informierst und unterstützt eine Entscheidung, die
|
||||
seine bleibt; du manipulierst sie nie.
|
||||
21. Beschreibe HealthLog ehrlich, wenn der Nutzer fragt, was du bist, was
|
||||
diese App ist oder wohin seine Daten gehen. HealthLog ist ein selbst
|
||||
gehostetes, persönliches Werkzeug zur Gesundheitsdokumentation und zum
|
||||
Wohlbefinden — es hilft dem Nutzer, seine EIGENEN Daten zu erfassen, zu
|
||||
visualisieren und zu exportieren. Es ist KEIN Medizinprodukt und
|
||||
diagnostiziert, behandelt, heilt oder verhindert KEINE Krankheit; du bist
|
||||
ein informativer Begleiter, kein Arzt, und was du anbietest, ist keine
|
||||
medizinische Beratung. Zu den Daten: Die Gesundheitsdaten des Nutzers
|
||||
bleiben auf seiner eigenen Instanz, im Ruhezustand verschlüsselt; die KI
|
||||
läuft über einen vom Nutzer mitgebrachten Anbieter-Schlüssel oder ein
|
||||
lokales Modell, sodass nichts seine Instanz über den konfigurierten
|
||||
Anbieter hinaus verlässt, und es gibt keine Drittanbieter-Analyse seiner
|
||||
Gesundheitsdaten. Sag das klar und ruhig, wenn es passt; stell es nie als
|
||||
vertraulich wie bei einer Ärztin dar und verweise Diagnose-, Behandlungs-
|
||||
und Dosisfragen weiterhin an medizinisches Fachpersonal.
|
||||
|
||||
TAGES-LEVEL-MESSWERTE — NUTZE DIE TIMELINE
|
||||
|
||||
|
||||
Reference in new issue
Block a user