import { useEffect, useState } from "react"; import { api } from "../../api.js"; import InfoHint from "./InfoHint.jsx"; import MiniLineChart from "./MiniLineChart.jsx"; /** * CustomerHealthSection — the three remaining signal consumers in one block. * * retention — D1 / D7 / D30 cohort return rates * satisfaction — NPS-style score from thumbs_up/thumbs_down (per-week trend) * engagement — behavioral segments (deep_divers, explorers, etc.) * * All three are GLOBAL (don't depend on the inspected user) and CUMULATIVE * in nature — they describe long-running product health metrics. The date * window selector applies via the `days` parameter. */ export default function CustomerHealthSection({ windowLabel = "", days = 30 }) { const [data, setData] = useState(null); const [err, setErr] = useState(null); const [busy, setBusy] = useState(false); async function load() { setBusy(true); setErr(null); try { const d = await api.customerHealth(days, 4); setData(d); } catch (e) { setErr(e.message); setData(null); } finally { setBusy(false); } } // Refetch on mount AND whenever the parent's window changes. useEffect(() => { load(); /* eslint-disable-next-line */ }, [days]); if (err) { return (
Failed /analytics/customer-health
{err}
); } if (!data) { return (
Loading customer health…
); } return (

Customer health {windowLabel}

Retention cohorts, NPS-style satisfaction score, and behavioral engagement segments — the three signal consumers that turn{" "} thumbs_up, thumbs_down,{" "} deeper_question, and session-continuity signals into product-health rollups.

); } // ─── Retention card ─────────────────────────────────────────────────────── function RetentionCard({ retention }) { const cohorts = retention?.cohorts || []; const d1 = retention?.overall_d1_retention ?? 0; const d7 = retention?.overall_d7_retention ?? 0; const d30 = retention?.overall_d30_retention ?? 0; return (

Retention

Cohort return-rate analysis. Users are grouped by the week they were first seen; D1 / D7 / D30 retention = % of that cohort that had at least one more turn within 1 / 7 / 30 days of their first visit. Only mature cohorts (old enough to measure) contribute to the overall average.
{cohorts.length === 0 ? (
No cohorts yet.
) : ( {cohorts.map((c) => ( ))}
Cohort (week) Size D1 D7 D30
{c.week_start} {c.size} {c.mature_d1 ? pct(c.rate_d1) : "—"} {c.mature_d7 ? pct(c.rate_d7) : "—"} {c.mature_d30 ? pct(c.rate_d30) : "—"}
)}
); } function RetentionPill({ label, rate }) { const tier = rate >= 0.5 ? "good" : rate >= 0.25 ? "mid" : "low"; return (
{pct(rate)}
{label}
); } // ─── Satisfaction card ──────────────────────────────────────────────────── function SatisfactionCard({ satisfaction }) { const s = satisfaction || {}; const nps = s.nps_score ?? 0; const verdict = s.verdict || "mixed"; return (

Satisfaction (NPS-style)

Score = (positive% − negative%) × 100 from thumbs_up / thumbs_down signals. Range [−100, +100].{" "} Very satisfied ≥ 50 · Mixed 0-50 · Concerning < 0. {" "}Note: thumbs_up/down are not used to update the bandit (they're ambiguous about cause) — but they ARE the cleanest signal for overall satisfaction.
{nps > 0 ? "+" : ""}{nps}
{prettyVerdict(verdict)}
Positive: {s.thumbs_up ?? 0} ({pct(s.positive_rate ?? 0)}) Negative: {s.thumbs_down ?? 0} ({pct(s.negative_rate ?? 0)}) {s.total_rated ?? 0} rated turns
{(s.weekly_trend || []).length > 1 && (
Weekly positive rate
({ date: w.week_start, value: Math.round(w.rate * 100), }))} width={300} height={70} color="#5fa86b" yLabel="positive %" formatValue={(v) => `${v}%`} />
)}
); } // ─── Engagement card ────────────────────────────────────────────────────── function EngagementCard({ engagement }) { const e = engagement || {}; const segments = e.segments || []; const total = e.total_users || 0; const max = Math.max(...segments.map((s) => s.count), 1); return (

Engagement segments

Behavioral segmentation. Each active user falls into exactly one segment based on their in-window pattern:

Deep divers: >5 deeper_questions on ≤3 topics — they want depth on a few subjects.
Explorers: >3 topics covered — broad curiosity.
Power users: >20 turns AND >2 sessions — habitual.
One-and-done: ≤2 turns lifetime — never came back after first try.
Casual: everyone else — moderate usage.
{total} active user{total === 1 ? "" : "s"} in window
{segments.length === 0 || total === 0 ? (
No engagement data yet.
) : ( )}
); } // ─── Helpers ────────────────────────────────────────────────────────────── function pct(v) { if (v == null || Number.isNaN(Number(v))) return "—"; return `${Math.round(Number(v) * 100)}%`; } function prettyVerdict(v) { return ({ very_satisfied: "Very satisfied", mixed: "Mixed", concerning: "Concerning", })[v] || v; } function prettySegment(s) { return ({ deep_divers: "Deep divers", explorers: "Explorers", power_users: "Power users", one_and_done: "One-and-done", casual: "Casual", })[s] || s.replace(/_/g, " "); }