"use client" /** * LeoInsightIndicator — reusable AI insight chip + popover. * * Usage: * * * * Two trigger layouts: * "toolbar" — compact "Insight" pill in a card header corner (default) * "plot-marker" — sits above an anchored data point on the chart canvas * * Palette is brand-only. Direction is communicated via icon SHAPE + kind LABEL * + signed delta value (never colour alone — WCAG 1.4.1). */ import * as React from "react" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Button } from "@/components/ui/button" import { Kbd } from "@/components/ui/kbd" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { AskLeoShortcutKbds, useAskLeo } from "@/components/ask-leo-sidebar" import { cn } from "@/lib/utils" // ───────────────────────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────────────────────── /** Optional anchor for drawing Leo on the plot (reference line + marker). */ export type ChartLeoInsightAnchor = { /** Categorical value on the chart's X axis (e.g. month label). */ xValue: string /** Fixed Y in data space; overrides yDataKeys / yCombine when set. */ yValue?: number /** Row keys to combine; required when yValue is omitted. */ yDataKeys?: string[] /** How to derive Y from yDataKeys: top of stacked bars = "sum", overlaid lines = "max". */ yCombine?: "max" | "sum" } /** * Semantic kind of the insight — drives color, icon, and chip label. * Defaults to "anomaly" when unset. */ export type ChartLeoInsightKind = "spike" | "dip" | "anomaly" | "trend" /** Smart scan copy for a chart — opens in a popover; CTA can prefill Ask Leo. */ export type ChartLeoInsight = { /** Short attention line (what stands out). */ headline: string /** Plain-language explanation. */ explanation: string /** Overrides the default prompt sent to Ask Leo. */ askLeoPrompt?: string /** * When set, pair with `ChartLeoPlotInsightOverlay` for an on-point pulse * + guide line + chip positioned directly on the chart. */ anchor?: ChartLeoInsightAnchor /** Semantic shape of the insight. Defaults to "anomaly". */ kind?: ChartLeoInsightKind /** Magnitude chip, e.g. `{ value: "-24%", label: "vs last Dec" }`. */ delta?: { value: string; label?: string } /** 2–4 supporting facts shown as bullets in the popover. */ bullets?: string[] /** Optional secondary quick-actions alongside the Ask Leo CTA. */ actions?: Array<{ label: string icon?: string onSelect?: () => void href?: string }> } // ───────────────────────────────────────────────────────────────────────────── // Internal constants // ───────────────────────────────────────────────────────────────────────────── const LEO_KIND_META: Record = { spike: { icon: "fa-arrow-trend-up", label: "Spike" }, dip: { icon: "fa-arrow-trend-down", label: "Dip" }, anomaly: { icon: "fa-wave-pulse", label: "Anomaly" }, trend: { icon: "fa-sparkles", label: "Insight" }, } export const LEO_TOKENS = { dotClass: "bg-brand", iconClass: "text-brand", softBgClass: "bg-brand/10", borderClass: "border-brand/50", cssVar: "var(--brand-color)", } as const function resolveLeoMeta(insight: ChartLeoInsight) { return LEO_KIND_META[insight.kind ?? "anomaly"] } // ───────────────────────────────────────────────────────────────────────────── // Component // ───────────────────────────────────────────────────────────────────────────── /** * Reusable Leo insight chip + popover. * * Renders a pill trigger button that opens a Radix Popover containing: * - Header: "Leo spotted" serif label + kind chip + close button * - Body: headline, delta label, explanation, optional bullets * - Footer: optional secondary actions + full-width "Ask Leo" CTA */ export function LeoInsightIndicator({ insight, chartTitle, triggerLayout = "toolbar", }: { insight: ChartLeoInsight chartTitle: string triggerLayout?: "toolbar" | "plot-marker" }) { const { openWithPrompt } = useAskLeo() const [open, setOpen] = React.useState(false) const titleId = React.useId() const descriptionId = React.useId() const defaultPrompt = insight.askLeoPrompt ?? `For the chart "${chartTitle}": ${insight.headline} — ${insight.explanation} What should we do next?` const meta = resolveLeoMeta(insight) const isPlot = triggerLayout === "plot-marker" const deltaValue = insight.delta?.value const directionLabel = insight.kind === "dip" ? "decreased" : insight.kind === "spike" ? "increased" : insight.kind === "anomaly" ? "anomaly detected" : "insight" const ariaFull = deltaValue ? `Leo insight: ${directionLabel} ${deltaValue}. ${insight.headline}.` : `Leo insight: ${insight.headline}.` return ( {/* Soft brand fill */} {deltaValue ? ( {deltaValue} ) : !isPlot ? ( Insight ) : null} {/* Ambient brand glow */} {/* Screen-reader announcement */} {`Leo spotted a ${meta.label.toLowerCase()}${deltaValue ? ` of ${deltaValue}` : ""}: ${insight.headline}`} {/* ── Header ─────────────────────────────────────────────────── */} {/* Serif "Leo spotted" heading */} Leo spotted {/* Kind + delta chip */} {meta.label} {deltaValue ? ( <> · {deltaValue} > ) : null} {/* Close — WCAG 2.5.5: 28×28 target + Tooltip */} setOpen(false)} className={cn( "icon-button-chrome inline-flex size-7 min-h-7 min-w-7 shrink-0 items-center justify-center rounded-md", "transition-colors hover:bg-muted hover:text-foreground", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", )} > Close Esc {/* ── Body ───────────────────────────────────────────────────── */} {insight.headline} {insight.delta?.label ? ( {insight.delta.label} ) : null} {insight.explanation} {insight.bullets && insight.bullets.length > 0 ? ( {insight.bullets.map((b, i) => ( {b} ))} ) : null} {/* ── Footer ─────────────────────────────────────────────────── */} {insight.actions?.map((a) => { const content = ( <> {a.icon ? ( ) : null} {a.label} > ) return ( { setOpen(false) a.onSelect?.() }} asChild={!!a.href} > {a.href ? {content} : content} ) })} { setOpen(false) openWithPrompt(defaultPrompt) }} > Ask Leo ) }
{insight.delta.label}
{insight.explanation}