import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { MessageSquare, Send, X } from "lucide-react"; import type { Message } from "@fusion/core"; import type { Agent } from "../api"; import { fetchConversation, sendMessage } from "../api"; import { useAgents } from "../hooks/useAgents"; interface QuickChatFABProps { projectId?: string; addToast: (msg: string, type?: "success" | "error") => void; } function getAgentLabel(agent: Agent): string { const base = agent.name?.trim() || agent.id; return `${base} (${agent.role})`; } export function QuickChatFAB({ projectId, addToast }: QuickChatFABProps) { const { agents } = useAgents(projectId); const [isOpen, setIsOpen] = useState(false); const [selectedAgentId, setSelectedAgentId] = useState(""); const [messages, setMessages] = useState([]); const [isConversationLoading, setIsConversationLoading] = useState(false); const [messageInput, setMessageInput] = useState(""); const [isSending, setIsSending] = useState(false); const panelRef = useRef(null); const fabRef = useRef(null); const messagesRef = useRef(null); useEffect(() => { if (agents.length === 0) { setSelectedAgentId(""); setMessages([]); return; } const selectedStillExists = agents.some((agent) => agent.id === selectedAgentId); if (!selectedStillExists) { setSelectedAgentId(agents[0]?.id ?? ""); } }, [agents, selectedAgentId]); const selectedAgent = useMemo( () => agents.find((agent) => agent.id === selectedAgentId) ?? null, [agents, selectedAgentId], ); const loadConversation = useCallback(async (agentId: string) => { if (!agentId) { setMessages([]); return; } setIsConversationLoading(true); try { const conversation = await fetchConversation(agentId, "agent", projectId); setMessages(conversation); } catch { addToast("Failed to load conversation", "error"); setMessages([]); } finally { setIsConversationLoading(false); } }, [addToast, projectId]); useEffect(() => { if (!isOpen || !selectedAgentId) return; void loadConversation(selectedAgentId); }, [isOpen, selectedAgentId, loadConversation]); useEffect(() => { if (!isOpen) return; const handleDocumentClick = (event: MouseEvent) => { const target = event.target as Node; if (panelRef.current?.contains(target)) return; if (fabRef.current?.contains(target)) return; setIsOpen(false); }; const handleEscape = (event: KeyboardEvent) => { if (event.key === "Escape") { setIsOpen(false); } }; document.addEventListener("mousedown", handleDocumentClick); document.addEventListener("keydown", handleEscape); return () => { document.removeEventListener("mousedown", handleDocumentClick); document.removeEventListener("keydown", handleEscape); }; }, [isOpen]); useEffect(() => { if (!isOpen) return; const messagesEl = messagesRef.current; if (!messagesEl) return; messagesEl.scrollTop = messagesEl.scrollHeight; }, [messages, isOpen]); const handleSendMessage = useCallback(async () => { const trimmed = messageInput.trim(); if (!selectedAgentId || !trimmed || isSending) return; setIsSending(true); try { await sendMessage( { toId: selectedAgentId, toType: "agent", content: trimmed, type: "user-to-agent", }, projectId, ); setMessageInput(""); await loadConversation(selectedAgentId); } catch { addToast("Failed to send message", "error"); } finally { setIsSending(false); } }, [addToast, isSending, loadConversation, messageInput, projectId, selectedAgentId]); const handleInputKeyDown = useCallback((event: ReactKeyboardEvent) => { if (event.key !== "Enter" || event.shiftKey) return; event.preventDefault(); void handleSendMessage(); }, [handleSendMessage]); if (agents.length === 0) { return null; } return ( <> {isOpen && (

Quick Chat

{isConversationLoading ? (
Loading conversation…
) : messages.length === 0 ? (
No messages yet. Start the conversation!
) : ( messages.map((message) => { const isSent = message.fromType === "user"; return (

{message.content}

); }) )}
setMessageInput(event.target.value)} onKeyDown={handleInputKeyDown} placeholder={selectedAgent ? `Message ${selectedAgent.name || selectedAgent.id}` : "Type a message"} disabled={!selectedAgentId || isSending} data-testid="quick-chat-input" />
)} ); }