"use client"; import { McpConfigModal } from "@/components/ui/mcp-config-modal"; import { Tooltip, TooltipProvider } from "@/components/ui/suggestions-tooltip"; import { cn } from "@/lib/utils"; import { useIsTamboTokenUpdating, useTamboThread, useTamboThreadInput, } from "@tambo-ai/react"; import { cva, type VariantProps } from "class-variance-authority"; import { ArrowUp, Square } from "lucide-react"; import * as React from "react"; /** * CSS variants for the message input container * @typedef {Object} MessageInputVariants * @property {string} default - Default styling * @property {string} solid - Solid styling with shadow effects * @property {string} bordered - Bordered styling with border emphasis */ const messageInputVariants = cva("w-full", { variants: { variant: { default: "", solid: [ "[&>div]:bg-background", "[&>div]:border-0", "[&>div]:shadow-xl [&>div]:shadow-black/5 [&>div]:dark:shadow-black/20", "[&>div]:ring-1 [&>div]:ring-black/5 [&>div]:dark:ring-white/10", "[&_textarea]:bg-transparent", "[&_textarea]:rounded-lg", ].join(" "), bordered: [ "[&>div]:bg-transparent", "[&>div]:border-2 [&>div]:border-gray-300 [&>div]:dark:border-zinc-600", "[&>div]:shadow-none", "[&_textarea]:bg-transparent", "[&_textarea]:border-0", ].join(" "), }, }, defaultVariants: { variant: "default", }, }); /** * @typedef MessageInputContextValue * @property {string} value - The current input value * @property {function} setValue - Function to update the input value * @property {function} submit - Function to submit the message * @property {function} handleSubmit - Function to handle form submission * @property {boolean} isPending - Whether a submission is in progress * @property {Error|null} error - Any error from the submission * @property {string|undefined} contextKey - The thread context key * @property {HTMLTextAreaElement|null} textareaRef - Reference to the textarea element * @property {string | null} submitError - Error from the submission * @property {function} setSubmitError - Function to set the submission error */ interface MessageInputContextValue { value: string; setValue: (value: string) => void; submit: (options: { contextKey?: string; streamResponse?: boolean; }) => Promise; handleSubmit: (e: React.FormEvent) => Promise; isPending: boolean; error: Error | null; contextKey?: string; textareaRef: React.RefObject; submitError: string | null; setSubmitError: React.Dispatch>; } /** * React Context for sharing message input data and functions among sub-components. * @internal */ const MessageInputContext = React.createContext(null); /** * Hook to access the message input context. * Throws an error if used outside of a MessageInput component. * @returns {MessageInputContextValue} The message input context value. * @throws {Error} If used outside of MessageInput. * @internal */ const useMessageInputContext = () => { const context = React.useContext(MessageInputContext); if (!context) { throw new Error( "MessageInput sub-components must be used within a MessageInput", ); } return context; }; /** * Props for the MessageInput component. * Extends standard HTMLFormElement attributes. */ export interface MessageInputProps extends React.HTMLAttributes { /** The context key identifying which thread to send messages to. */ contextKey?: string; /** Optional styling variant for the input container. */ variant?: VariantProps["variant"]; /** The child elements to render within the form container. */ children?: React.ReactNode; } /** * The root container for a message input component. * It establishes the context for its children and handles the form submission. * @component MessageInput * @example * ```tsx * * * * * * ``` */ const MessageInput = React.forwardRef( ({ children, className, contextKey, variant, ...props }, ref) => { const { value, setValue, submit, isPending, error } = useTamboThreadInput(); const { cancel } = useTamboThread(); const [displayValue, setDisplayValue] = React.useState(""); const [submitError, setSubmitError] = React.useState(null); const [isSubmitting, setIsSubmitting] = React.useState(false); const textareaRef = React.useRef(null); React.useEffect(() => { setDisplayValue(value); if (value && textareaRef.current) { textareaRef.current.focus(); } }, [value]); const handleSubmit = React.useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!value.trim() || isSubmitting) return; setSubmitError(null); setDisplayValue(""); setIsSubmitting(true); try { await submit({ contextKey, streamResponse: true, }); setValue(""); setTimeout(() => { textareaRef.current?.focus(); }, 0); } catch (error) { console.error("Failed to submit message:", error); setDisplayValue(value); setSubmitError( error instanceof Error ? error.message : "Failed to send message. Please try again.", ); // Cancel the thread to reset loading state cancel(); } finally { setIsSubmitting(false); } }, [ value, submit, contextKey, setValue, setDisplayValue, setSubmitError, cancel, isSubmitting, ], ); const contextValue = React.useMemo( () => ({ value: displayValue, setValue: (newValue: string) => { setValue(newValue); setDisplayValue(newValue); }, submit, handleSubmit, isPending: isPending ?? isSubmitting, error, contextKey, textareaRef, submitError, setSubmitError, }), [ displayValue, setValue, submit, handleSubmit, isPending, isSubmitting, error, contextKey, submitError, ], ); return (
{children}
); }, ); MessageInput.displayName = "MessageInput"; /** * Props for the MessageInputTextarea component. * Extends standard TextareaHTMLAttributes. */ export interface MessageInputTextareaProps extends React.TextareaHTMLAttributes { /** Custom placeholder text. */ placeholder?: string; } /** * Textarea component for entering message text. * Automatically connects to the context to handle value changes and key presses. * @component MessageInput.Textarea * @example * ```tsx * * * * ``` */ const MessageInputTextarea = ({ className, placeholder = "What do you want to do?", ...props }: MessageInputTextareaProps) => { const { value, setValue, textareaRef, handleSubmit } = useMessageInputContext(); const { isIdle } = useTamboThread(); const isUpdatingToken = useIsTamboTokenUpdating(); const isPending = !isIdle; const handleChange = (e: React.ChangeEvent) => { setValue(e.target.value); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if (value.trim()) { handleSubmit(e as unknown as React.FormEvent); } } }; return (