import { useState, useCallback, useMemo, useEffect, useRef } from "react"; import { useAutosizeTextarea } from "../hooks/useAutosizeTextarea"; import { X, Send, Loader2, Bot, AlertCircle } from "lucide-react"; import type { ParticipantType, MessageType } from "@fusion/core"; import { getErrorMessage } from "@fusion/core"; import { sendMessage } from "../api"; import type { Agent } from "../api"; // ── Types ───────────────────────────────────────────────────────────────── interface MessageComposerProps { /** Pre-fill recipient (e.g. when replying) */ recipient?: { id: string; type: ParticipantType } | null; /** Reply context for linked replies */ replyContext?: { messageId: string; preview?: string } | null; /** List of agents for recipient selection */ agents?: Agent[]; /** Project ID for multi-project */ projectId?: string; /** Called when message is successfully sent */ onSend: () => void; /** Called when user cancels */ onCancel: () => void; /** Toast notification callback */ addToast?: (msg: string, type?: "success" | "error") => void; /** Loading state for agents (shows placeholder) */ isLoadingAgents?: boolean; } const MAX_CONTENT_LENGTH = 2000; // ── Component ───────────────────────────────────────────────────────────── export function MessageComposer({ recipient, replyContext, agents = [], projectId, onSend, onCancel, addToast, isLoadingAgents = false, }: MessageComposerProps) { const [toId, setToId] = useState(recipient?.id ?? ""); const [toType, setToType] = useState(recipient?.type ?? "agent"); const [content, setContent] = useState(""); const [wakeRecipient, setWakeRecipient] = useState(false); const [isSending, setIsSending] = useState(false); const [error, setError] = useState(null); const textareaRef = useRef(null); const { ref: autosizeRef } = useAutosizeTextarea({ value: content, minHeight: 68, maxHeight: 320, }); const setTextareaRef = useCallback((node: HTMLTextAreaElement | null) => { textareaRef.current = node; autosizeRef(node); }, [autosizeRef]); const selectedAgent = useMemo(() => agents.find((agent) => agent.id === toId), [agents, toId]); const prefilledRecipientAgent = useMemo( () => (recipient ? agents.find((agent) => agent.id === recipient.id) : undefined), [agents, recipient], ); const recipientIsAgent = toType === "agent"; const recipientAlwaysImmediate = recipientIsAgent && selectedAgent?.runtimeConfig?.messageResponseMode === "immediate"; const wakeImmediately = recipientIsAgent && (wakeRecipient || recipientAlwaysImmediate); const isValid = toId.trim() !== "" && content.trim().length > 0 && content.length <= MAX_CONTENT_LENGTH; const handleSend = useCallback(async () => { if (!isValid || isSending) return; setIsSending(true); setError(null); try { const messageType: MessageType = toType === "agent" ? "user-to-agent" : "system"; const metadata = replyContext ? { replyTo: { messageId: replyContext.messageId } } : undefined; const sendWakeImmediately = wakeImmediately; await sendMessage( { toId: toId.trim(), toType, content: content.trim(), type: messageType, ...(metadata ? { metadata } : {}), ...(sendWakeImmediately ? { wakeImmediately: true } : {}), }, projectId, ); onSend(); } catch (err) { const msg = getErrorMessage(err) || "Failed to send message"; setError(msg); addToast?.(msg, "error"); } finally { setIsSending(false); } }, [isValid, isSending, toId, toType, content, wakeImmediately, replyContext, projectId, onSend, addToast]); const handleAgentSelect = useCallback((agentId: string) => { setToId(agentId); setToType("agent"); }, []); useEffect(() => { if (!replyContext) { return; } textareaRef.current?.focus(); }, [replyContext]); useEffect(() => { if (!replyContext || typeof window === "undefined" || window.visualViewport == null) { return; } const handleVisualViewportResize = () => { textareaRef.current?.scrollIntoView({ block: "center", behavior: "auto" }); }; window.visualViewport.addEventListener("resize", handleVisualViewportResize); return () => { window.visualViewport?.removeEventListener("resize", handleVisualViewportResize); }; }, [replyContext]); return (
{replyContext ? "Reply" : "New Message"}
{/* Recipient selection */} {!recipient && (
)} {/* Recipient display (when pre-filled from reply) */} {recipient && (
To: {prefilledRecipientAgent?.name || recipient.id}
)} {replyContext && (
Replying to: {replyContext.preview?.trim() ? replyContext.preview : `Message ${replyContext.messageId}`}
)} {/* Content */}