import * as React from "react"; import { CircleAlert, RefreshCw, Zap, Clock, FileText, UserRound, Briefcase, CalendarClock, ExternalLink, Check, ChevronDown, Sparkles, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; import { Chip } from "@/components/ui/chip"; import { TypingDots } from "@/components/ui/chat-widget-primitives"; /** * Copilot Primitives — WealthX DS (Atoms) * * Display-only response atoms for the broker Copilot extension chat surface. * Each renders one "response case" the agent can return. No API calls, no * state beyond local UI — data in via props, events out via callbacks. * * Composes existing DS primitives (Alert, Chip, Button) rather than re-styling * raw markup, so they inherit the system's variants, tokens and sharp corners. * * Exports: * CopilotErrorMessage — failed response + Retry (Alert destructive) * CopilotRateLimitMessage — usage/quota exhausted (Alert warning) * CopilotSourceCitation — "Based on:" data-source chips (Chip) */ // --------------------------------------------------------------------------- // CopilotErrorMessage // --------------------------------------------------------------------------- export interface CopilotErrorMessageProps { /** Headline shown to the user. Defaults to a generic failure message. */ message?: string; /** Optional technical/contextual reason shown under the headline. */ detail?: string; /** When provided, renders a Retry button that calls this. */ onRetry?: () => void; /** Label for the retry button. Defaults to "Retry". */ retryLabel?: string; className?: string; } export function CopilotErrorMessage({ message = "Something went wrong generating a response.", detail, onRetry, retryLabel = "Retry", className, }: CopilotErrorMessageProps) { return ( {message} {(detail || onRetry) && ( {detail &&

{detail}

} {onRetry && ( )}
)}
); } // --------------------------------------------------------------------------- // CopilotRateLimitMessage // --------------------------------------------------------------------------- export interface CopilotRateLimitMessageProps { /** Headline. Defaults to a usage-limit message. */ message?: string; /** Human-readable reset hint, e.g. "in 12 minutes" or "at 2:30 PM". */ resetLabel?: string; /** When provided, renders an upgrade/primary action. */ onUpgrade?: () => void; /** Label for the upgrade action. Defaults to "Upgrade plan". */ upgradeLabel?: string; className?: string; } export function CopilotRateLimitMessage({ message = "You've reached your Copilot usage limit.", resetLabel, onUpgrade, upgradeLabel = "Upgrade plan", className, }: CopilotRateLimitMessageProps) { return ( {message} {(resetLabel || onUpgrade) && ( {resetLabel && (

)} {onUpgrade && ( )}
)}
); } // --------------------------------------------------------------------------- // CopilotSourceCitation // --------------------------------------------------------------------------- export type CopilotSourceType = "contact" | "deal" | "document" | "appointment"; export interface CopilotSource { /** Display text, e.g. "Jordan Avery" or "Loan #4821". */ label: string; /** Drives the leading icon. Defaults to "document". */ type?: CopilotSourceType; /** When provided, the chip becomes interactive and calls this. */ onClick?: () => void; } export interface CopilotSourceCitationProps { sources: CopilotSource[]; /** Leading label. Defaults to "Based on". */ label?: string; className?: string; } const SOURCE_ICON: Record< CopilotSourceType, React.ComponentType<{ className?: string }> > = { contact: UserRound, deal: Briefcase, document: FileText, appointment: CalendarClock, }; export function CopilotSourceCitation({ sources, label = "Based on", className, }: CopilotSourceCitationProps) { if (sources.length === 0) return null; return (
{label}: {sources.map((source, i) => { const Icon = SOURCE_ICON[source.type ?? "document"]; const interactive = Boolean(source.onClick); return ( ) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); source.onClick?.(); } }, } : {})} > {source.label} {interactive && ( )} ); })}
); } // --------------------------------------------------------------------------- // CopilotThinkingSteps // --------------------------------------------------------------------------- export interface CopilotThinkingStepsProps { /** * Reasoning steps. While `isThinking`, the last entry is the in-progress step * (animated dots) and earlier ones are completed; otherwise all are completed. */ steps: string[]; /** * Collapsed summary line. Defaults to the live step while thinking, or * "Thought process" once done. */ summary?: string; /** Reasoning still in progress — animated, last step active. @default true */ isThinking?: boolean; /** Start expanded. @default false */ defaultExpanded?: boolean; className?: string; } /** * Agent "thinking" disclosure (Claude/GPT style): a collapsed one-line summary * by default; clicking the chevron expands the full reasoning trace. While * `isThinking`, the summary tracks the live step and the last step animates. * Ports the policy-AI thinking-steps pattern into a shared, collapsible atom. */ export function CopilotThinkingSteps({ steps, summary, isThinking = true, defaultExpanded = false, className, }: CopilotThinkingStepsProps) { const [expanded, setExpanded] = React.useState(defaultExpanded); if (steps.length === 0) return null; const current = steps[steps.length - 1]; // While the agent is still reasoning, show a plain live line (the current // step with animated dots) — no disclosure chevron, nothing to expand yet. if (isThinking) { const liveText = summary ?? current; return (
{liveText}
); } // Once done, collapse into a "Thought process" disclosure that expands the // full (completed) reasoning trace. const summaryText = summary ?? "Thought process"; return (
{expanded && (
    {steps.map((step, i) => (
  1. ))}
)}
); }