feat(insights): polished summary typography with inline-chart sparklines

Bumps the InsightAdvisorCard summary surface to text-base / leading-
relaxed and constrains it to max-w-prose so the prose stays readable
on a wide viewport. Wraps the existing chart-token renderer in a
new mini flag so the summary's inline charts shrink to sparkline
form (h-[140px], no range tabs); per-finding inline charts keep the
full chart so the user can still interact with the data series.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 02:01:15 +02:00
1 parent 9e8be4b359
commit b76351e1a8
2 files changed
+73 -9

No files matched your search

@@ -177,3 +177,47 @@ describe("<InsightAdvisorCard> — inline chart tokens", () => {
expect(html).toContain("Weight stable");
});
});
describe("<InsightAdvisorCard> — summary typography polish (B1b)", () => {
it("paints the summary slot with a constrained max-width for readability", () => {
const html = render(
buildInsight({
summary: "Your BP averaged 132/84 over the last 30 days.",
}),
);
expect(html).toMatch(/data-slot="insight-summary"/);
// The summary slot carries a Tailwind max-w-* class so prose
// doesn't stretch full-bleed on a wide viewport.
expect(html).toMatch(/max-w-(prose|2xl|3xl)/);
});
it("uses the polished prose typography (text-base, leading-relaxed)", () => {
const html = render(
buildInsight({
summary: "All metrics within target.",
}),
);
// A larger font + relaxed line-height for the page-anchored
// summary; the per-card content uses text-sm.
expect(html).toMatch(
/data-slot="insight-summary"[^>]*class="[^"]*text-base/,
);
expect(html).toMatch(
/data-slot="insight-summary"[^>]*class="[^"]*leading-relaxed/,
);
});
it("renders summary inline charts as mini sparklines (data-mini=true)", () => {
const html = render(
buildInsight({
summary: "BP averaging 132/84 metric:BLOOD_PRESSURE_SYS today",
}),
);
// The summary's inline-chart wrapper must carry `data-mini="true"`
// so the chart shrinks to sparkline form. Per-finding charts use
// the default detail mode.
expect(html).toMatch(
/data-slot="insight-summary"[\s\S]*?data-mini="true"/,
);
});
});
@@ -229,7 +229,19 @@ const INLINE_CHART_TITLE_KEYS: Record<string, string> = {
COMPLIANCE: "insights.navMedication",
};
function InlineCharts({ tokens }: { tokens: ChartToken[] }) {
function InlineCharts({
tokens,
mini = false,
}: {
tokens: ChartToken[];
/**
* v1.4.16 phase B1b — render the embedded charts as compact
* sparklines (no range tabs, h-[140px]). The summary surface uses
* this so the prose stays the headline; the finding-card surface
* keeps the full chart so the user can interact with the data.
*/
mini?: boolean;
}) {
const { t } = useTranslations();
if (tokens.length === 0) return null;
@@ -255,11 +267,12 @@ function InlineCharts({ tokens }: { tokens: ChartToken[] }) {
key={token}
data-slot="insight-inline-chart"
data-metric={metric}
data-mini={mini ? "true" : "false"}
>
{kind === "mood" ? (
<MoodChart />
<MoodChart mini={mini} />
) : (
<HealthChart types={[metric]} title={title} />
<HealthChart types={[metric]} title={title} mini={mini} />
)}
</div>
);
@@ -431,12 +444,19 @@ export function InsightAdvisorCard({
{/* Summary — prose first, then any allowlisted charts the model
* inlined via `metric:<TYPE>` tokens. Tokens are dropped from the
* visible text so the literal substring never reaches the DOM. */}
<div className="space-y-3">
<p className="text-muted-foreground text-sm leading-relaxed">
{stripChartTokens(insight.summary)}
</p>
<InlineCharts tokens={parseChartTokens(insight.summary)} />
* visible text so the literal substring never reaches the DOM.
*
* v1.4.16 phase B1b — typography polish: larger font (text-base),
* relaxed line-height, max-w-prose so the summary doesn't span
* full-bleed on a wide viewport. Wraps the inline-chart strip
* in the same constrained column so the rendered sparklines
* align with the prose. */}
<div
data-slot="insight-summary"
className="text-foreground/90 max-w-prose space-y-3 text-base leading-relaxed"
>
<p>{stripChartTokens(insight.summary)}</p>
<InlineCharts tokens={parseChartTokens(insight.summary)} mini />
</div>
{/* Findings — top finding gets a hero treatment, rest are a compact list */}