import * as React from "react"; import { Clock, Plus } from "lucide-react"; import { cn } from "@/lib/utils"; import { ChatWidgetHeader, ChatWidgetMessage, } from "@/components/ui/chat-widget-primitives"; import { ChatInputArea } from "@/components/ui/chat-input-area"; import { Button } from "@/components/ui/button"; import { CopilotChatHistory, type CopilotConversation, } from "@/components/ui/copilot-chat-history"; import { CopilotErrorMessage, CopilotRateLimitMessage, CopilotSourceCitation, CopilotThinkingSteps, type CopilotErrorMessageProps, type CopilotRateLimitMessageProps, type CopilotSource, } from "@/components/ui/copilot-primitives"; import { CopilotActionCard, type CopilotActionCardProps, } from "@/components/ui/copilot-action-card"; import { CopilotPlanCard, type CopilotPlanCardProps, } from "@/components/ui/copilot-plan-card"; import { CopilotDataCard, type CopilotDataCardProps, } from "@/components/ui/copilot-data-card"; import { CopilotSuggestionCard, type CopilotSuggestionCardProps, } from "@/components/ui/copilot-suggestion-card"; import { CopilotContextCard, type CopilotContextCardProps, } from "@/components/ui/copilot-context-card"; import { CopilotMeetingNoteCard, type CopilotMeetingNoteCardProps, } from "@/components/ui/copilot-meeting-note-card"; import { CopilotMeetingRecordPrompt, type CopilotMeetingRecordPromptProps, } from "@/components/ui/copilot-meeting-record-prompt"; import { CopilotConnectionState } from "@/components/ui/copilot-connection-state"; import { CopilotAuthState } from "@/components/ui/copilot-auth-state"; import { CopilotEmptyState, type CopilotSuggestedPrompt, } from "@/components/ui/copilot-empty-state"; /** * CopilotPanel — WealthX DS (Template) * * The full broker Copilot extension panel: a branded header, a body that * switches between the connection / signed-out / empty / chat states, and a * message composer. The chat thread composes every Copilot response atom and * molecule. Layout/composition only — driven by mock/placeholder data via props. */ export type CopilotPanelState = | "not-installed" | "connecting" | "signed-out" | "empty" | "chat"; interface CopilotThreadMessage { role: "bot" | "advisor" | "user" | "system"; content: string; format?: "plain" | "markdown"; timestamp?: string; /** Source chips rendered beneath the message. */ sources?: CopilotSource[]; } export type CopilotThreadItem = | ({ kind: "message" } & CopilotThreadMessage) | { kind: "thinking"; steps?: string[]; isThinking?: boolean; defaultExpanded?: boolean; } | ({ kind: "action" } & CopilotActionCardProps) | ({ kind: "plan" } & CopilotPlanCardProps) | ({ kind: "data" } & CopilotDataCardProps) | ({ kind: "suggestion" } & CopilotSuggestionCardProps) | ({ kind: "context" } & CopilotContextCardProps) | ({ kind: "meeting-note" } & CopilotMeetingNoteCardProps) | ({ kind: "meeting-record" } & CopilotMeetingRecordPromptProps) | ({ kind: "error" } & CopilotErrorMessageProps) | ({ kind: "rate-limit" } & CopilotRateLimitMessageProps); export interface CopilotPanelProps { /** Which surface to show. Defaults to "chat". */ state?: CopilotPanelState; brokerName?: string; subtitle?: string; onMinimize?: () => void; // chat thread items?: CopilotThreadItem[]; // connection state onInstall?: () => void; onContinue?: () => void; // signed-out state onSignIn?: () => void; // empty state prompts?: CopilotSuggestedPrompt[]; // composer (chat state) inputValue?: string; onInputChange?: (value: string) => void; onSend?: (value: string) => void; inputPlaceholder?: string; inputDisabled?: boolean; /** Show the markdown formatting toolbar in the composer. @default true */ showMarkdownToolbar?: boolean; /** When provided, shows an attach-file button in the composer. */ onAttachFile?: (files: FileList) => void; // chat history (opens as a bottom sheet from the history icon) conversations?: CopilotConversation[]; currentConversationId?: string; onSelectConversation?: (id: string) => void; onRenameConversation?: (id: string) => void; onDeleteConversation?: (id: string) => void; /** When provided, shows a "new chat" (+) control next to the history icon. */ onNewChat?: () => void; className?: string; } /** A card that sits on the right (user-supplied) rather than the agent side. */ const RIGHT_ALIGNED_KINDS = new Set(["context"]); /** Maps a card-style thread item's `kind` to the component that renders it. */ const CARD_BY_KIND: Record> = { action: CopilotActionCard, plan: CopilotPlanCard, data: CopilotDataCard, suggestion: CopilotSuggestionCard, context: CopilotContextCard, "meeting-note": CopilotMeetingNoteCard, "meeting-record": CopilotMeetingRecordPrompt, error: CopilotErrorMessage, "rate-limit": CopilotRateLimitMessage, }; function ThreadItem({ item }: { item: CopilotThreadItem }) { if (item.kind === "message") { const { kind, sources, ...message } = item; void kind; const isRight = message.role === "user"; return (
{sources && sources.length > 0 && (
)}
); } if (item.kind === "thinking") { return ( ); } // Card-style items: look the renderer up by kind and align by side. const { kind, ...props } = item; const Card = CARD_BY_KIND[kind]; const isRight = RIGHT_ALIGNED_KINDS.has(kind); return (
); } export function CopilotPanel({ state = "chat", brokerName = "WealthX Copilot", subtitle = "Your AI assistant", onMinimize, items = [], onInstall, onContinue, onSignIn, prompts, inputValue = "", onInputChange, onSend, inputPlaceholder, inputDisabled, showMarkdownToolbar = true, onAttachFile, conversations, currentConversationId, onSelectConversation, onRenameConversation, onDeleteConversation, onNewChat, className, }: CopilotPanelProps) { const [historyOpen, setHistoryOpen] = React.useState(false); return (
{(state === "not-installed" || state === "connecting") && ( )} {state === "signed-out" && ( )} {state === "empty" && ( )} {state === "chat" && (
{items.map((item, i) => ( ))}
)}
{(state === "chat" || state === "empty") && ( <>
{onNewChat && ( )}
{})} onSend={onSend ?? (() => {})} placeholder={inputPlaceholder} disabled={inputDisabled} showMarkdownToolbar={showMarkdownToolbar} onAttachFile={onAttachFile} />
)} {/* Chat history — bottom sheet scoped to the panel. Hand-rolled rather than the DS Sheet because Sheet portals to document.body and can't be contained inside the panel's own bounds. */} {historyOpen && (
)}
); }