diff --git a/src/components/onboarding/BaselineForm.tsx b/src/components/onboarding/BaselineForm.tsx
index b5b591f89..404f46fed 100644
--- a/src/components/onboarding/BaselineForm.tsx
+++ b/src/components/onboarding/BaselineForm.tsx
@@ -11,7 +11,15 @@ import { Button } from "@/components/ui/button";
import { DateInput } from "@/components/ui/date-input";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import { useTranslations } from "@/lib/i18n/context";
+import { readError } from "@/lib/api/read-error";
/**
* v1.4.25 W14b-Content — onboarding step 3 (baseline).
@@ -166,16 +174,32 @@ export function BaselineForm() {
-
@@ -225,14 +249,3 @@ export function BaselineForm() {
);
}
-async function readError(res: Response): Promise {
- try {
- const json = (await res.json()) as { error?: string };
- if (typeof json.error === "string" && json.error.length > 0) {
- return json.error;
- }
- } catch {
- /* fall through */
- }
- return `Request failed (${res.status})`;
-}
diff --git a/src/components/onboarding/GoalsChipPicker.tsx b/src/components/onboarding/GoalsChipPicker.tsx
index 09b8d421e..8274f834c 100644
--- a/src/components/onboarding/GoalsChipPicker.tsx
+++ b/src/components/onboarding/GoalsChipPicker.tsx
@@ -17,6 +17,7 @@ import {
import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
+import { readError } from "@/lib/api/read-error";
import { cn } from "@/lib/utils";
/**
@@ -230,14 +231,3 @@ export function GoalsChipPicker({ userId: _userId }: GoalsChipPickerProps) {
);
}
-async function readError(res: Response): Promise {
- try {
- const json = (await res.json()) as { error?: string };
- if (typeof json.error === "string" && json.error.length > 0) {
- return json.error;
- }
- } catch {
- /* fall through */
- }
- return `Request failed (${res.status})`;
-}
diff --git a/src/components/onboarding/SourceCardGrid.tsx b/src/components/onboarding/SourceCardGrid.tsx
index 53c559f9b..3067336f9 100644
--- a/src/components/onboarding/SourceCardGrid.tsx
+++ b/src/components/onboarding/SourceCardGrid.tsx
@@ -15,6 +15,7 @@ import {
import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
+import { readError } from "@/lib/api/read-error";
import { cn } from "@/lib/utils";
/**
@@ -309,14 +310,3 @@ function SourceCardItem({
);
}
-async function readError(res: Response): Promise {
- try {
- const json = (await res.json()) as { error?: string };
- if (typeof json.error === "string" && json.error.length > 0) {
- return json.error;
- }
- } catch {
- /* fall through */
- }
- return `Request failed (${res.status})`;
-}
diff --git a/src/components/onboarding/WelcomeCarousel.tsx b/src/components/onboarding/WelcomeCarousel.tsx
index f9db1ced2..3f2899ee6 100644
--- a/src/components/onboarding/WelcomeCarousel.tsx
+++ b/src/components/onboarding/WelcomeCarousel.tsx
@@ -15,6 +15,7 @@ import {
import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
+import { readError } from "@/lib/api/read-error";
import { cn } from "@/lib/utils";
/**
@@ -294,14 +295,3 @@ export function WelcomeCarousel() {
);
}
-async function readError(res: Response): Promise {
- try {
- const json = (await res.json()) as { error?: string };
- if (typeof json.error === "string" && json.error.length > 0) {
- return json.error;
- }
- } catch {
- /* fall through */
- }
- return `Request failed (${res.status})`;
-}
diff --git a/src/components/settings/advanced-section.tsx b/src/components/settings/advanced-section.tsx
index 4979507bd..855a64da7 100644
--- a/src/components/settings/advanced-section.tsx
+++ b/src/components/settings/advanced-section.tsx
@@ -20,6 +20,10 @@ import { Switch } from "@/components/ui/switch";
import { ResearchModeAcknowledgmentDialog } from "@/components/medications/ResearchModeAcknowledgmentDialog";
import { formatDateTime } from "@/lib/format";
import { useTranslations } from "@/lib/i18n/context";
+import {
+ type ResearchModeStatus,
+ researchModeGateState,
+} from "@/lib/medications/research-mode-types";
/**
* Settings → Advanced.
@@ -67,11 +71,27 @@ export function AdvancedSection() {
);
}
-interface ResearchModeStatus {
- enabled: boolean;
- acknowledgedAt: string | null;
- acknowledgedVersion: string | null;
- currentDisclaimerVersion: string;
+/**
+ * v1.4.25 W21 Fix-N (simp-H3) — pull the nested ternary that decided
+ * the toggle's caption out of the JSX. The four-way ladder (loading /
+ * enabled-open / enabled-stale / disabled) is dense to read inline,
+ * and the surface stays open to a fifth state ("never queried") when
+ * we wire the offline shell in v1.4.26.
+ */
+function researchModeStatusLabel(
+ status: ResearchModeStatus | null | undefined,
+ isLoading: boolean,
+ t: (key: string, params?: Record) => string,
+): string {
+ if (isLoading) return t("common.loading");
+ const gate = researchModeGateState(status);
+ if (gate === "off") return t("settings.researchMode.disabledStatus");
+ if (gate === "stale") return t("settings.researchMode.enabledStaleStatus");
+ return t("settings.researchMode.acknowledgedOn", {
+ date: status?.acknowledgedAt
+ ? formatDateTime(status.acknowledgedAt)
+ : "—",
+ });
}
function ResearchModeCard() {
@@ -92,10 +112,8 @@ function ResearchModeCard() {
staleTime: 60 * 1000,
});
- const versionsAligned =
- !!status &&
- status.acknowledgedVersion === status.currentDisclaimerVersion;
- const showRePrompt = !!status?.enabled && !versionsAligned;
+ const gateState = researchModeGateState(status);
+ const showRePrompt = gateState === "stale";
// The Switch's `checked` mirrors the server flag exactly — even when
// the version is stale we keep the toggle "on" so the user sees that
// their previous choice is preserved; the banner above the toggle
@@ -178,17 +196,7 @@ function ResearchModeCard() {
className="text-muted-foreground text-xs"
data-slot="settings-research-mode-status"
>
- {isLoading
- ? t("common.loading")
- : status?.enabled
- ? versionsAligned
- ? t("settings.researchMode.acknowledgedOn", {
- date: status.acknowledgedAt
- ? formatDateTime(status.acknowledgedAt)
- : "—",
- })
- : t("settings.researchMode.enabledStaleStatus")
- : t("settings.researchMode.disabledStatus")}
+ {researchModeStatusLabel(status, isLoading, t)}