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
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user