"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 (