import * as React from "react"; import { Bot, UserRound, MessageCircle, X, ChevronDown, Send, Smile, Paperclip, } from "lucide-react"; import { cn } from "@/lib/utils"; import { resolveBrandVars } from "@/lib/colors"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { MarkdownContent } from "@/components/ui/markdown-content"; /** * ChatWidget Primitives — WealthX DS (Atoms) * * Atomic building blocks for the embeddable broker website chat widget. * All atoms are pure display components — no API calls, no state beyond local UI. * * Exports: * ChatWidgetLauncher — Floating action button (open/close) * ChatWidgetHeader — Gradient branded header * ChatWidgetMessage — Message bubble (bot | user | system) * TypingDots — Bare three-bouncing-dots animation (shared atom) * ChatWidgetTypingIndicator — Animated 3-dot indicator * ChatWidgetInputBar — Footer input with emoji/attachment/send */ // --------------------------------------------------------------------------- // ChatWidgetLauncher // --------------------------------------------------------------------------- export interface ChatWidgetLauncherProps { /** Whether the chat window is currently open. */ isOpen: boolean; onClick: () => void; /** * Solid brand color (hex) for the button background. Omit to inherit * `--primary` from the nearest ThemeProvider. When set, the icon color is * derived from it so it stays readable on light brand colors. */ brandColor?: string; /** Number of unread messages — shows a red badge when > 0 and chat is closed. */ unreadCount?: number; className?: string; } export function ChatWidgetLauncher({ isOpen, onClick, brandColor, unreadCount, className, }: ChatWidgetLauncherProps) { return ( ); } // --------------------------------------------------------------------------- // ChatWidgetHeader // --------------------------------------------------------------------------- export interface ChatWidgetHeaderProps { brokerName: string; subtitle?: string; /** Gradient start color. */ gradientFrom?: string; /** Gradient end color. */ gradientTo?: string; onMinimize?: () => void; className?: string; } export function ChatWidgetHeader({ brokerName, subtitle = "Feel free to chat with us", gradientFrom, gradientTo, onMinimize, className, }: ChatWidgetHeaderProps) { const hasGradient = gradientFrom && gradientTo; return (

{brokerName}

{subtitle && (

{subtitle}

)}
{onMinimize && ( )}
); } // --------------------------------------------------------------------------- // ChatWidgetMessage // --------------------------------------------------------------------------- export type ChatWidgetMessageRole = "bot" | "advisor" | "user" | "system"; export interface ChatWidgetMessageProps { role: ChatWidgetMessageRole; content: string; timestamp?: string; /** True while the bot response is still streaming in — shows TypingIndicator instead. */ isStreaming?: boolean; /** * How to render `content`. `"plain"` (default) renders the raw string. * `"markdown"` renders it as sanitized markdown (headings, lists, links, * tables, code) — use for AI / assistant responses. */ format?: "plain" | "markdown"; className?: string; } export function ChatWidgetMessage({ role, content, timestamp, isStreaming, format = "plain", className, }: ChatWidgetMessageProps) { if (role === "system") { return (
{content}
); } const isLeft = role === "bot" || role === "advisor"; return (
{isLeft && ( {role === "advisor" ? ( ) : ( )} )}
{isStreaming ? ( ) : format === "markdown" ? ( ) : ( content )}
{timestamp && !isStreaming && ( {timestamp} )}
); } // --------------------------------------------------------------------------- // TypingDots // --------------------------------------------------------------------------- export interface TypingDotsProps { /** * Size preset. `"default"` = size-1.5 rounded dots at /60 opacity (chat * widget); `"sm"` = size-1 square dots at /70 opacity (copilot thinking * steps). Bundles dot size, gap, shape and opacity so call sites stay * pixel-identical to their prior hand-rolled markup. */ size?: "default" | "sm"; className?: string; } /** * Bare three-bouncing-dots animation, shared by `ChatWidgetTypingIndicator` * and the copilot `CopilotThinkingSteps`. Renders an `aria-hidden` span — the * accessible status text is owned by the consuming component. */ export function TypingDots({ size = "default", className }: TypingDotsProps) { const sm = size === "sm"; return (