Merge branch 'fix/375-generated-narratives' into release/v1.25.0

This commit is contained in:
Marc-André Bombeck committed 2026-06-28 14:42:46 +02:00
commit c0a91dbbcd
19 files changed
+643 -19

No files matched your search

@@ -238,4 +238,55 @@ describe("<DailyBriefing>", () => {
expect(html).toContain('data-slot="daily-briefing-findings"');
expect(html).not.toContain('data-slot="daily-briefing-no-provider"');
});
// v1.25 — generation-failed affordances. The briefing keeps its last good
// text; the card adds an honest "couldn't refresh" footer hint on a held
// briefing and a "couldn't generate — retry" empty state when there is none.
it("renders the refresh-failed footer hint on a held briefing", () => {
const html = render(
<DailyBriefing
briefing={baseBriefing}
onRegenerate={() => {}}
generationFailed
/>,
);
expect(html).toContain('data-slot="daily-briefing-refresh-failed"');
// The rendered apostrophe is HTML-escaped; match the unambiguous tail.
expect(html).toContain("refresh the briefing");
expect(html).toContain('data-slot="daily-briefing-refresh-failed-retry"');
});
it("does NOT render the refresh-failed hint when generation did not fail", () => {
const html = render(
<DailyBriefing briefing={baseBriefing} onRegenerate={() => {}} />,
);
expect(html).not.toContain('data-slot="daily-briefing-refresh-failed"');
});
it("suppresses the refresh-failed hint when no provider is connected", () => {
const html = render(
<DailyBriefing
briefing={baseBriefing}
onRegenerate={() => {}}
generationFailed
noProviderStale
/>,
);
expect(html).not.toContain('data-slot="daily-briefing-refresh-failed"');
expect(html).toContain('data-slot="daily-briefing-stale-no-provider"');
});
it("renders the 'couldn't generate' empty state with a retry CTA when failed and empty", () => {
const html = render(
<DailyBriefing
briefing={null}
onRegenerate={() => {}}
generationFailed
/>,
);
// The rendered apostrophe is HTML-escaped; match the unambiguous tail.
expect(html).toContain("generate the briefing");
expect(html).toMatch(/data-slot="daily-briefing-empty-cta"/);
expect(html).toContain("Retry");
});
});
+53 -4
View File
@@ -119,6 +119,14 @@ interface DailyBriefingProps {
* the staleness reads as intentional, not as a live report.
*/
noProviderStale?: boolean;
/**
* v1.25 — the most recent generation attempt failed (provider timeout /
* error). The briefing keeps its last good text, so this never blanks a
* shown card; it only adds an honest "couldn't refresh" hint to the footer
* of a held briefing, and — when there is no briefing to show — swaps the
* generic empty state for a "couldn't generate" one whose CTA retries.
*/
generationFailed?: boolean;
/**
* Optional slot for a meta control mounted in the card header — the
* comparison toggle migrates here from the hero in commit 5.
@@ -349,6 +357,7 @@ export function DailyBriefing({
regenerating = false,
noProvider = false,
noProviderStale = false,
generationFailed = false,
metaSlot,
memory = null,
}: DailyBriefingProps) {
@@ -468,7 +477,7 @@ export function DailyBriefing({
</div>
</div>
)}
{(updatedAt || noProviderStale) && (
{(updatedAt || noProviderStale || generationFailed) && (
<div className="border-border/60 space-y-1.5 border-t pt-3">
{updatedAt && (
<p
@@ -505,6 +514,32 @@ export function DailyBriefing({
</Link>
</p>
)}
{/* v1.25 — the held briefing is shown, but the last refresh
attempt failed (provider timeout / error). State it plainly
and offer a retry so the staleness reads as held, not
current. Suppressed when no provider is configured (that
hint owns the footer) — a retry would be futile there. */}
{generationFailed && !noProviderStale && (
<p
data-slot="daily-briefing-refresh-failed"
className="text-muted-foreground flex flex-wrap items-center justify-end gap-x-1.5 gap-y-1 text-right text-xs"
>
<span>
{t("insights.dailyBriefing.refreshFailedHint")}
</span>
{onRegenerate && (
<button
type="button"
onClick={onRegenerate}
disabled={regenerating}
data-slot="daily-briefing-refresh-failed-retry"
className="text-foreground/80 hover:text-foreground underline underline-offset-2 disabled:opacity-60"
>
{t("insights.dailyBriefing.retryAction")}
</button>
)}
</p>
)}
</div>
)}
{/* v1.21.0 (C4 H2) — hand off to the Coach for the whole
@@ -542,8 +577,20 @@ export function DailyBriefing({
data-slot="daily-briefing-empty"
variant="plain"
icon={<FileText className="size-5" />}
title={t("insights.dailyBriefing.emptyTitle")}
description={t("insights.dailyBriefing.emptyDescription")}
// v1.25 — when the last attempt FAILED and there is no last good
// text to fall back on, say so honestly ("couldn't generate")
// rather than the generic "no briefing yet". The CTA below is the
// same explicit regenerate — it retries the failed generation.
title={
generationFailed
? t("insights.dailyBriefing.failedTitle")
: t("insights.dailyBriefing.emptyTitle")
}
description={
generationFailed
? t("insights.dailyBriefing.failedDescription")
: t("insights.dailyBriefing.emptyDescription")
}
action={
onRegenerate ? (
// v1.4.28 BK-M2 — switch the empty-state CTA from
@@ -564,7 +611,9 @@ export function DailyBriefing({
<span>
{regenerating
? t("insights.heroRegenerating")
: t("insights.dailyBriefing.emptyAction")}
: generationFailed
? t("insights.dailyBriefing.retryAction")
: t("insights.dailyBriefing.emptyAction")}
</span>
</Button>
) : null
@@ -71,6 +71,15 @@ export interface InsightAdvisorPayload {
* predates the field — treated as "provider present" (no hint).
*/
hasProvider?: boolean;
/**
* v1.25 — true when the most recent generation attempt failed (a failure
* marker newer than the last successful generation). The briefing keeps its
* last good text on failure, so this is the honest signal that a shown
* briefing is held, or — with no last good text — that the empty state
* should read "couldn't generate" with a retry rather than the generic one.
* Absent on a pre-field cached payload → treated as "not failed" (no hint).
*/
generationFailed?: boolean;
}
/**
@@ -279,6 +288,13 @@ export interface UseInsightsAdvisorResult {
* or an unsettled query.
*/
hasProvider: boolean;
/**
* v1.25 — true when the GET reported the last generation attempt failed.
* Pairs a shown-but-held briefing with a discreet "couldn't refresh" hint,
* and swaps the generic empty state for a "couldn't generate — retry" one
* when there is no last good text. Defaults false (no hint) when absent.
*/
generationFailed: boolean;
}
/**
@@ -364,5 +380,8 @@ export function useInsightsAdvisorQuery(
// assume a provider is present so a transient unknown never flashes a
// false "no provider" hint.
hasProvider: query.data?.payload?.hasProvider ?? true,
// Absent (pre-field cached payload or unsettled query) → not failed, so a
// transient unknown never flashes a false "couldn't refresh" hint.
generationFailed: query.data?.payload?.generationFailed ?? false,
};
}