import * as React from "react"; import { Bot, RotateCcw, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { Sheet, SheetContent } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ChatInputArea } from "@/components/ui/chat-input-area"; import { Spinner } from "@/components/ui/spinner"; /** * AiAssistantDrawer — WealthX DS (L5 Drawer) * * Right-side panel that provides an AI conversation interface for a specific * opportunity/deal. Used in the Pipeline board via the "Launch Assistant" button * on OpportunityCard. * * Layout: * • Header — bot icon + title + reload + close * • Content — empty state (task suggestions) or chat message list * • Footer — textarea input + send button * * Pure display component: all message state and API calls are managed by the * consuming page. The drawer only handles local `inputValue` state. * * Data source: `ai-chat.ts` / `conversation.ts` hooks in the backoffice */ // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface AiChatMessage { id: string; role: "user" | "assistant"; content: string; /** True while the assistant response is still streaming in. */ isStreaming?: boolean; /** True if the message failed to send or receive. */ isErrored?: boolean; } export interface AiTaskSuggestion { id: string; /** Short label shown as the suggestion title. */ title: string; /** Supporting description shown below the title. */ description: string; } export interface AiAssistantDrawerProps { open: boolean; onClose: () => void; /** Opportunity or contact name shown in the header subtitle. */ opportunityName?: string; /** * Suggested tasks shown in the empty state. * Typically the opportunity's incomplete tasks. * Clicking a suggestion pre-fills the input. */ taskSuggestions?: AiTaskSuggestion[]; /** Chat message history. Empty array = show empty/welcome state. */ messages?: AiChatMessage[]; /** True while the assistant is generating a response. */ isStreaming?: boolean; /** True while initial data is loading (shows full-panel spinner). */ isLoading?: boolean; /** Called when the user submits a message. Input is cleared after this fires. */ onSendMessage?: (text: string) => void; /** Called when the user selects files via the attachment button. */ onAttachFile?: (files: FileList) => void; /** Called when the user selects images via the image upload button. */ onAttachImage?: (files: FileList) => void; /** Called when the user clicks the reload/reset button. */ onReset?: () => void; className?: string; } // --------------------------------------------------------------------------- // Typing indicator (three bouncing dots) // --------------------------------------------------------------------------- function AiTypingIndicator() { return ( {[0, 150, 300].map((delay) => ( ); } // --------------------------------------------------------------------------- // Chat message bubble // --------------------------------------------------------------------------- function AiChatBubble({ message }: { message: AiChatMessage }) { const isUser = message.role === "user"; const isEmpty = !message.content.trim(); return (
{/* Content or streaming indicators */} {isEmpty && message.isStreaming ? ( ) : ( {message.content} )} {message.isErrored && (

Failed to send. Please try again.

)}
); } // --------------------------------------------------------------------------- // Task suggestion card // --------------------------------------------------------------------------- function AiTaskCard({ suggestion, onSelect, }: { suggestion: AiTaskSuggestion; onSelect: (text: string) => void; }) { return ( ); } // --------------------------------------------------------------------------- // AiAssistantDrawer // --------------------------------------------------------------------------- const DEFAULT_SUGGESTIONS: AiTaskSuggestion[] = [ { id: "s1", title: "Update pricing strategy", description: "Review and adjust pricing based on market trends", }, { id: "s2", title: "Enhance marketing outreach", description: "Develop new campaigns to increase customer engagement", }, { id: "s3", title: "Optimise social media strategy", description: "Analyse performance and adjust content for better visibility", }, { id: "s4", title: "Implement customer feedback loops", description: "Create mechanisms to collect and act on customer insights", }, ]; export function AiAssistantDrawer({ open, onClose, opportunityName, taskSuggestions, messages = [], isStreaming = false, isLoading = false, onSendMessage, onAttachFile, onAttachImage, onReset, className, }: AiAssistantDrawerProps) { const [inputValue, setInputValue] = React.useState(""); const messagesEndRef = React.useRef(null); const suggestions = taskSuggestions ?? DEFAULT_SUGGESTIONS; const hasMessages = messages.length > 0; // Auto-scroll to latest message React.useEffect(() => { if (!messagesEndRef.current) return; messagesEndRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", }); }, [messages.length]); const handleSend = React.useCallback( (text: string) => { onSendMessage?.(text); setInputValue(""); }, [onSendMessage], ); const handleSuggestionSelect = (text: string) => { setInputValue(text); }; return ( !o && onClose()}> {/* Header */}
AI Assistant {opportunityName && ( {opportunityName} )}
{onReset && ( )}
{/* Content */}
{isLoading ? ( /* Loading state */

Initialising…

) : !hasMessages ? ( /* Empty / welcome state */
{/* Hero card */}

A safe and secure way to chat about insights that run your business and utilise AI. All chats stay within your environment, all closed off and compliant.

{( [ { emoji: "🤖", label: "Smart" }, { emoji: "⚡", label: "Fast" }, { emoji: "🔒", label: "Secure" }, ] as const ).map(({ emoji, label }) => ( {emoji} {label} ))}
{/* Task suggestions */} {suggestions.length > 0 && (

Suggested tasks

{suggestions.map((s) => ( ))}
)}
) : ( /* Message list */
{messages.map((msg) => ( ))} {/* Streaming indicator when last message is user's */} {isStreaming && messages[messages.length - 1]?.role === "user" && (
)}
)}
{/* Footer — input bar */}
); }