mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
chore(dashboard): retire the standalone InsightsCardPreview surface
The dashboard preview duplicated the much-richer /insights advisor surface that already lives one click away (Daily Briefing, hero strip, correlation row, full advisor, weekly-report banner). The preview also competed visually with the GLP-1 tile for the same vertical real estate above the chart row. Drop the import + render + isChartVisible gate in src/app/page.tsx, drop the insightsPreview widget entry from src/lib/dashboard-layout.ts, drop the matching row in src/components/settings/dashboard-layout-section.tsx, and delete src/components/insights/insights-card.tsx and its test file. The advisor query hook stays on /insights — the dashboard no longer mounts it. Update the dashboard-layout contract test to assert the widget id is absent. F7 weekly-report scan: ran grep -rinE 'Wochenreport|weekly report|/insights/report' across src/ and messages/. All weekly-report click affordances on /insights are correctly wired (HeroStrip Generate button → currentWeekHref, WeeklyReportBanner → /insights/report/[week], InsightAdvisorCard banner → /insights/report/[week]). The /insights/report/[week] route mounts WeeklyReportView and renders the full surface. No dead click affordance was located inside the 30-minute scan budget. The retired InsightsCardPreview was the only dashboard-anchored insight card with a CTA — its removal in this commit is the most likely reason the maintainer perceived a dead click. F7 deferred to v1.4.28 with a maintainer-screenshot ask; recorded in .planning/v1428-backlog.md. i18n keys still referenced after this change (B6 sweep will reconcile): none from this commit. Keys 'dashboard.insightsPreview' and 'insights.aiInsights' are now unreferenced in code and become candidates for B6's dead-key sweep.
This commit is contained in:
1 parent
50150e35ef
commit
9afb31bd15
8 files changed
+26
-349
No files matched your search
+11
-37
@@ -46,8 +46,6 @@ import { summaryToTrend7Delta } from "@/lib/analytics/trend-delta";
|
||||
import { GettingStartedChecklist } from "@/components/onboarding/getting-started-checklist";
|
||||
import { TourLauncher } from "@/components/onboarding/tour-launcher";
|
||||
import { RecentAchievementsCard } from "@/components/gamification/recent-achievements-card";
|
||||
import { InsightsCardPreview } from "@/components/insights/insights-card";
|
||||
import { useInsightsAdvisorQuery } from "@/components/insights/use-insights-advisor";
|
||||
import { Glp1Tile } from "@/components/dashboard/glp1-tile";
|
||||
|
||||
const HealthChart = dynamic(
|
||||
@@ -234,16 +232,13 @@ export default function DashboardPage() {
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
// v1.4.16 phase D reconcile (CRITICAL C2) — pull the rich advisor
|
||||
// payload so the dashboard can surface a compact preview of the top
|
||||
// severity-ordered AI recommendations + ring confidence meter +
|
||||
// "View all" CTA. Shares the cache with /insights via the
|
||||
// queryKeys.insightsAdvisor() key so a regenerate on either surface
|
||||
// refreshes the other without a second LLM round-trip. Returns null
|
||||
// when the user has no provider configured (route 422 → null), so
|
||||
// the preview self-hides without burning rate-limit tokens on
|
||||
// unconfigured accounts.
|
||||
const advisor = useInsightsAdvisorQuery(isAuthenticated);
|
||||
// v1.4.27 B1 — the dashboard's `<InsightsCardPreview>` retired (it
|
||||
// duplicated the much-richer `/insights` advisor surface). The advisor
|
||||
// query lives on `/insights` directly; the dashboard no longer needs
|
||||
// its own hook subscription, so the local `useInsightsAdvisorQuery`
|
||||
// call dropped with the preview.
|
||||
|
||||
// TODO(B1+B4 reconcile): wire per-tile availability gate using hasMetricData
|
||||
|
||||
const w = data?.summaries?.WEIGHT;
|
||||
const sys = data?.summaries?.BLOOD_PRESSURE_SYS;
|
||||
@@ -340,12 +335,6 @@ export default function DashboardPage() {
|
||||
// the layout-toggle gate here. No data-floor check (the empty card is
|
||||
// intentional — the maintainer wants the user to discover the feature).
|
||||
const showAchievementsCard = isChartVisible("achievements");
|
||||
// v1.4.16 phase D reconcile (CRITICAL C2) — gate the dashboard
|
||||
// insights preview by the layout toggle. The component itself
|
||||
// returns null when the advisor payload is missing or has no
|
||||
// recommendations, so the gate is sufficient — no extra data-floor
|
||||
// check needed.
|
||||
const showInsightsPreview = isChartVisible("insightsPreview");
|
||||
|
||||
// Glucose widget — visible iff layout enables it AND at least one reading exists.
|
||||
// Glucose has no separate chart slot today, so the tile flag is the
|
||||
@@ -1247,29 +1236,14 @@ export default function DashboardPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* v1.4.16 phase D reconcile (CRITICAL C2) — dashboard
|
||||
preview of the polished AI recommendations surface.
|
||||
Pinned above the chart row (out-of-band from the sorted
|
||||
charts[] array) so it stays at the top regardless of
|
||||
widget reorder operations. Self-hides when the user has
|
||||
no provider configured OR no recommendations to surface
|
||||
(`<InsightsCardPreview>` returns null), so the preview
|
||||
doesn't paint an empty card on first visit. */}
|
||||
{showInsightsPreview && (
|
||||
<InsightsCardPreview
|
||||
insight={advisor.payload?.insights ?? null}
|
||||
/>
|
||||
)}
|
||||
{/* v1.4.25 W6 — GLP-1 status tile. The tile self-gates on
|
||||
`Medication.treatmentClass === "GLP1"` (route returns
|
||||
`data: null` when the user has no active GLP-1 med), so
|
||||
we always mount it and let the tile suppress itself.
|
||||
Slot is below the InsightsCardPreview and above the
|
||||
chart row — high enough that a Mounjaro / Ozempic user
|
||||
sees their dose-response chart without scrolling past
|
||||
BP / pulse, but below the AI preview so the latter
|
||||
stays the "scroll-stop hero" Marc anchored the dashboard
|
||||
around. */}
|
||||
v1.4.27 — the standalone insights preview retired (it
|
||||
duplicated the much-richer `/insights` advisor surface);
|
||||
the GLP-1 tile now anchors the top of the chart-row
|
||||
stack. */}
|
||||
<Glp1Tile />
|
||||
{charts.map((entry) => (
|
||||
<div key={entry.id} className="space-y-2">
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { InsightsCardPreview } from "../insights-card";
|
||||
import type { InsightResult, InsightRecommendation } from "@/lib/ai/types";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B1b — dashboard InsightsCard preview.
|
||||
*
|
||||
* The preview is a compact wrapper that surfaces the top 1-2
|
||||
* severity-ordered recommendations + a "View all" CTA pointing to
|
||||
* the full `/insights` page. Visual language matches the page hero
|
||||
* + recommendation grid (Dracula tokens, severity-coloured borders,
|
||||
* mini confidence meter).
|
||||
*/
|
||||
|
||||
vi.mock("@/components/charts/health-chart", () => ({
|
||||
HealthChart: () => <div data-testid="health-chart-mock" />,
|
||||
}));
|
||||
vi.mock("@/components/charts/mood-chart", () => ({
|
||||
MoodChart: () => <div data-testid="mood-chart-mock" />,
|
||||
}));
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: null, isLoading: false }),
|
||||
useMutation: () => ({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
}),
|
||||
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
|
||||
}));
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({
|
||||
user: { id: "test-user", username: "tester", role: "USER" },
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
function rec(
|
||||
id: string,
|
||||
severity: "info" | "suggestion" | "important" | "urgent",
|
||||
text: string,
|
||||
confidence?: number,
|
||||
): InsightRecommendation {
|
||||
return {
|
||||
id,
|
||||
text,
|
||||
severity,
|
||||
confidence,
|
||||
rationale: {
|
||||
dataWindow: "last7days",
|
||||
comparedTo: "your 90-day median",
|
||||
deviation: "moderate deviation",
|
||||
},
|
||||
metricSource: {
|
||||
type: "weight",
|
||||
timeRange: "last7days",
|
||||
summary: "84.2 kg avg over 7 days",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildInsight(recommendations: InsightRecommendation[]): InsightResult {
|
||||
return {
|
||||
insightType: "general",
|
||||
summary: "Test summary",
|
||||
classification: "gut",
|
||||
classificationLabel: "Good",
|
||||
findings: [],
|
||||
correlations: [],
|
||||
primaryRecommendation: "",
|
||||
recommendations,
|
||||
dataQuality: { coverage: "good", gaps: [], confidence: "hoch" },
|
||||
disclaimer: "Not medical advice.",
|
||||
};
|
||||
}
|
||||
|
||||
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<InsightsCardPreview>", () => {
|
||||
it("surfaces only the top 2 severity-ordered recommendations", () => {
|
||||
const recs = [
|
||||
rec("a", "info", "info-rec"),
|
||||
rec("b", "urgent", "urgent-rec"),
|
||||
rec("c", "important", "important-rec"),
|
||||
rec("d", "suggestion", "suggestion-rec"),
|
||||
];
|
||||
const html = render(<InsightsCardPreview insight={buildInsight(recs)} />);
|
||||
expect(html).toContain("urgent-rec");
|
||||
expect(html).toContain("important-rec");
|
||||
// The preview must hide the remaining recs — they live on /insights.
|
||||
expect(html).not.toContain("info-rec");
|
||||
expect(html).not.toContain("suggestion-rec");
|
||||
});
|
||||
|
||||
it("renders the 'View all' CTA pointing to /insights", () => {
|
||||
const recs = [rec("a", "urgent", "urgent-rec")];
|
||||
const html = render(<InsightsCardPreview insight={buildInsight(recs)} />);
|
||||
expect(html).toMatch(/href="\/insights"/);
|
||||
expect(html).toMatch(/data-slot="insights-card-view-all"/);
|
||||
});
|
||||
|
||||
it("paints a severity-coloured left border on each preview card", () => {
|
||||
const recs = [
|
||||
rec("a", "urgent", "urgent-rec"),
|
||||
rec("b", "important", "important-rec"),
|
||||
];
|
||||
const html = render(<InsightsCardPreview insight={buildInsight(recs)} />);
|
||||
expect(html).toMatch(/border-l-dracula-red/);
|
||||
expect(html).toMatch(/border-l-dracula-orange/);
|
||||
});
|
||||
|
||||
it("renders a mini confidence meter inline when confidence is supplied", () => {
|
||||
const recs = [rec("a", "urgent", "urgent-rec", 78)];
|
||||
const html = render(<InsightsCardPreview insight={buildInsight(recs)} />);
|
||||
// The ConfidenceMeter component renders a 'data-slot="confidence-meter"'
|
||||
// marker; the preview shows the inline ring/bars variant.
|
||||
expect(html).toMatch(/data-slot="confidence-meter"/);
|
||||
});
|
||||
|
||||
it("hides the preview entirely when there are no recommendations", () => {
|
||||
const html = render(<InsightsCardPreview insight={buildInsight([])} />);
|
||||
// The orphan "no recs" state is delegated to the full /insights page.
|
||||
// The preview either renders the top recs OR doesn't mount at all.
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("passes a null insight transparently — no preview", () => {
|
||||
const html = render(<InsightsCardPreview insight={null} />);
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("renders the title slot so dashboard nav can sit alongside", () => {
|
||||
const recs = [rec("a", "urgent", "urgent-rec")];
|
||||
const html = render(<InsightsCardPreview insight={buildInsight(recs)} />);
|
||||
expect(html).toMatch(/data-slot="insights-card-preview"/);
|
||||
// Card title text from the existing translation key.
|
||||
expect(html).toContain("AI Insights");
|
||||
});
|
||||
|
||||
it("German locale — title + view-all CTA translate", () => {
|
||||
const recs = [rec("a", "urgent", "urgent-rec")];
|
||||
const html = render(
|
||||
<InsightsCardPreview insight={buildInsight(recs)} />,
|
||||
"de",
|
||||
);
|
||||
expect(html).toContain("KI-Insights");
|
||||
// "View all" → German "Alle ansehen"
|
||||
expect(html).toContain("Alle ansehen");
|
||||
});
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Sparkles, ArrowRight } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { stripChartTokens } from "@/lib/insights/chart-tokens";
|
||||
import type { InsightResult } from "@/lib/ai/types";
|
||||
import { sortRecommendationsBySeverity } from "./recommendations-grid";
|
||||
import { ConfidenceMeter } from "./confidence-meter";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B1b — dashboard <InsightsCardPreview>.
|
||||
*
|
||||
* Compact preview of the top 1-2 severity-ordered recommendations
|
||||
* + a "View all" CTA pointing at `/insights`. Visual language matches
|
||||
* the page hero (Dracula gradient sparkle, severity-coloured left
|
||||
* border, mini confidence meter inline) so the dashboard reads as
|
||||
* a smaller window onto the same surface.
|
||||
*
|
||||
* Design notes:
|
||||
* - The preview is dashboard-anchored, so it doesn't carry the full
|
||||
* rationale-expand mechanic. Recs render as one-row tiles.
|
||||
* - When the preview has zero recs OR no insight at all, the
|
||||
* component returns `null` so the dashboard's parent card grid
|
||||
* doesn't render an empty box. The full empty-state lives on
|
||||
* `/insights` proper.
|
||||
* - The legacy `InsightsCard` (which fetched its own data + rendered
|
||||
* the v1.4.0 changed/stable/drivers/nextSteps shape) was an
|
||||
* orphan component with zero non-test imports — it's been replaced
|
||||
* by this leaner preview.
|
||||
*/
|
||||
|
||||
interface InsightsCardPreviewProps {
|
||||
insight: InsightResult | null;
|
||||
/** Maximum number of recs to surface; defaults to 2. */
|
||||
topN?: number;
|
||||
}
|
||||
|
||||
const SEVERITY_BORDER_CLASSES: Record<string, string> = {
|
||||
urgent: "border-l-dracula-red/70",
|
||||
important: "border-l-dracula-orange/70",
|
||||
suggestion: "border-l-dracula-purple/70",
|
||||
info: "border-l-dracula-cyan/70",
|
||||
};
|
||||
|
||||
const SEVERITY_BORDER_FALLBACK = "border-l-border";
|
||||
|
||||
export function InsightsCardPreview({
|
||||
insight,
|
||||
topN = 2,
|
||||
}: InsightsCardPreviewProps) {
|
||||
const { t } = useTranslations();
|
||||
if (!insight || insight.recommendations.length === 0) return null;
|
||||
|
||||
const ordered = sortRecommendationsBySeverity(insight.recommendations).slice(
|
||||
0,
|
||||
topN,
|
||||
);
|
||||
|
||||
return (
|
||||
<Card data-slot="insights-card-preview" className="overflow-hidden">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles
|
||||
className="text-dracula-purple h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CardTitle className="text-base">
|
||||
{t("insights.aiInsights")}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-slot="insights-card-view-all"
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Link href="/insights">
|
||||
{t("insights.viewAll")}
|
||||
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2">
|
||||
{ordered.map((rec, index) => {
|
||||
const text = typeof rec === "string" ? rec : rec.text;
|
||||
const severity = typeof rec === "string" ? null : rec.severity;
|
||||
const confidence =
|
||||
typeof rec === "string" ? undefined : rec.confidence;
|
||||
const id = typeof rec === "string" ? `${index}-${rec}` : rec.id;
|
||||
const borderClass =
|
||||
(severity && SEVERITY_BORDER_CLASSES[severity]) ||
|
||||
SEVERITY_BORDER_FALLBACK;
|
||||
return (
|
||||
<li
|
||||
key={id ?? index}
|
||||
className={`bg-card/40 flex items-start justify-between gap-2 rounded-md border-l-2 px-3 py-2 ${borderClass}`}
|
||||
>
|
||||
<p className="text-sm leading-snug">{stripChartTokens(text)}</p>
|
||||
{typeof confidence === "number" && (
|
||||
<span className="shrink-0">
|
||||
<ConfidenceMeter value={confidence} variant="ring" />
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -23,9 +23,9 @@ import { queryKeys } from "@/lib/query-keys";
|
||||
* C1+C2 specifically because it assumed that duplication was required,
|
||||
* but the route already supports the cache-aware path.
|
||||
*
|
||||
* Both `/insights` (`<InsightAdvisorCard>`) and `/` (`<InsightsCardPreview>`)
|
||||
* mount the query under the same key, so a regenerate from either surface
|
||||
* refreshes the other without a second LLM call.
|
||||
* `/insights` (`<InsightAdvisorCard>`) and any other surface that mounts
|
||||
* the query under the same key share the cache, so a regenerate from
|
||||
* one surface refreshes the others without a second LLM call.
|
||||
*/
|
||||
export interface InsightAdvisorPayload {
|
||||
insights: InsightResult;
|
||||
|
||||
@@ -44,10 +44,6 @@ const WIDGET_LABEL_KEYS: Record<DashboardWidgetId, string> = {
|
||||
bpInTarget: "dashboard.bpInTarget",
|
||||
oxygenSaturation: "measurements.typeOxygenSaturation",
|
||||
achievements: "achievements.title",
|
||||
// v1.4.16 phase D reconcile (CRITICAL C2) — dashboard preview tile
|
||||
// for the polished `<InsightsCardPreview>` (top severity-ordered AI
|
||||
// recommendations + ring confidence meter + "View all" CTA).
|
||||
insightsPreview: "dashboard.insightsPreview",
|
||||
// v1.4.25 W8d — VO2 max secondary-metric tile (opt-in).
|
||||
vo2Max: "dashboard.vo2Max",
|
||||
};
|
||||
|
||||
@@ -141,13 +141,13 @@ describe("DEFAULT_DASHBOARD_LAYOUT contract", () => {
|
||||
expect(ids).toContain("achievements");
|
||||
});
|
||||
|
||||
it("includes the insightsPreview widget (v1.4.16 phase D reconcile C2)", () => {
|
||||
// CRITICAL C2: dashboard preview of the polished AI recommendations
|
||||
// surface. Same Zod-enum-drift trap as A5 — if the widget id isn't
|
||||
// listed here, the API PUT silently 422s and the layout toggle
|
||||
// never persists. Pin the contract to catch a regression early.
|
||||
it("does NOT advertise a retired insightsPreview widget (v1.4.27 B1)", () => {
|
||||
// v1.4.27 B1 retired the standalone dashboard preview because it
|
||||
// duplicated the much-richer `/insights` advisor surface. The
|
||||
// widget id must stay out of the default layout so a stale client
|
||||
// cannot reintroduce it via a layout PUT.
|
||||
const ids = DEFAULT_DASHBOARD_LAYOUT.widgets.map((w) => w.id);
|
||||
expect(ids).toContain("insightsPreview");
|
||||
expect(ids).not.toContain("insightsPreview");
|
||||
});
|
||||
|
||||
it("survives a round-trip through serialize → resolve unchanged", () => {
|
||||
|
||||
@@ -29,11 +29,6 @@ export const DASHBOARD_WIDGET_IDS = [
|
||||
"bpInTarget",
|
||||
"oxygenSaturation",
|
||||
"achievements",
|
||||
// v1.4.16 phase D reconcile (CRITICAL C2) — dashboard preview of the
|
||||
// top severity-ordered AI recommendations + "View all" CTA. Has only
|
||||
// a `visible` (chart-row) surface; tileVisible is forced false because
|
||||
// there's no tile-strip representation for it.
|
||||
"insightsPreview",
|
||||
// v1.4.25 W8d — VO2 max trend tile (opt-in, secondary-metric pattern).
|
||||
// Self-gates on having any VO2_MAX sample; default-invisible so
|
||||
// existing dashboards stay unchanged until the user enables it in
|
||||
@@ -216,22 +211,11 @@ export const DEFAULT_DASHBOARD_LAYOUT: DashboardLayout = {
|
||||
// `tileVisible` is forced false because there is no tile surface
|
||||
// for this widget; only the chart-row card.
|
||||
{ id: "achievements", visible: true, tileVisible: false, order: 13 },
|
||||
// v1.4.16 phase D reconcile (CRITICAL C2) — AI insights preview
|
||||
// anchored at the top of the chart row. Default visible so users
|
||||
// discover the feature without hunting through Settings →
|
||||
// Dashboard; self-gates on having a generated payload AND at least
|
||||
// one recommendation (`<InsightsCardPreview>` returns null
|
||||
// otherwise). `tileVisible` forced false because the preview lives
|
||||
// in the chart row, not the tile strip. The dashboard page
|
||||
// renders this entry independently of the sorted `charts[]` array
|
||||
// so it stays pinned above charts even if the user reorders other
|
||||
// widgets; only the boolean visibility flag is consumed.
|
||||
{ id: "insightsPreview", visible: true, tileVisible: false, order: 14 },
|
||||
// v1.4.25 W8d — VO2 max trend tile. Default-invisible on both
|
||||
// surfaces; opt-in via Settings → Dashboard. Only the tile-strip
|
||||
// representation today (`visible` stays false so the chart-row
|
||||
// gate never fires; the chart card lands in v1.5).
|
||||
{ id: "vo2Max", visible: false, tileVisible: false, order: 15 },
|
||||
{ id: "vo2Max", visible: false, tileVisible: false, order: 14 },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ export const queryKeys = {
|
||||
insightsComprehensive: () => ["insights", "comprehensive"] as const,
|
||||
insightsTargets: () => ["insights", "targets"] as const,
|
||||
/**
|
||||
* v1.4.16 phase D reconcile (CRITICAL C1 + C2) — shared cache key for
|
||||
* the rich `/api/insights/generate` advisor payload. Both `/insights`
|
||||
* (full `<InsightAdvisorCard>`) and `/` (`<InsightsCardPreview>`) read
|
||||
* this key so a regenerate on one surface refreshes the other without
|
||||
* a second LLM round-trip.
|
||||
* Shared cache key for the rich `/api/insights/generate` advisor
|
||||
* payload. `/insights` (full `<InsightAdvisorCard>`) and any other
|
||||
* surface that subscribes under this key share the same cache so a
|
||||
* regenerate on one surface refreshes the others without a second
|
||||
* LLM round-trip.
|
||||
*/
|
||||
insightsAdvisor: () => ["insights", "advisor"] as const,
|
||||
insightsBpStatus: (locale: string) =>
|
||||
|
||||
Reference in new issue
Block a user