'use client'; import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle, } from 'react'; import { useAgents } from '@/hooks/react-query/agents/use-agents'; import { useAgentSelection } from '@/lib/stores/agent-selection-store'; import { Card, CardContent } from '@/components/ui/card'; import { handleFiles } from './file-upload-handler'; import { MessageInput } from './message-input'; import { AttachmentGroup } from '../attachment-group'; import { useModelSelection } from './_use-model-selection-new'; import { useFileDelete } from '@/hooks/react-query/files'; import { useQueryClient } from '@tanstack/react-query'; import { ToolCallInput } from './floating-tool-preview'; import { ChatSnack } from './chat-snack'; import { Brain, Zap, Workflow, Database, ArrowDown } from 'lucide-react'; import { useComposioToolkitIcon } from '@/hooks/react-query/composio/use-composio'; import { Skeleton } from '@/components/ui/skeleton'; import { IntegrationsRegistry } from '@/components/agents/integrations-registry'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { useSubscriptionData } from '@/contexts/SubscriptionContext'; import { isLocalMode } from '@/lib/config'; import { BillingModal } from '@/components/billing/billing-modal'; import { useRouter } from 'next/navigation'; import posthog from 'posthog-js'; export interface ChatInputHandles { getPendingFiles: () => File[]; clearPendingFiles: () => void; } export interface ChatInputProps { onSubmit: ( message: string, options?: { model_name?: string; enable_thinking?: boolean; agent_id?: string; }, ) => void; placeholder?: string; loading?: boolean; disabled?: boolean; isAgentRunning?: boolean; onStopAgent?: () => void; autoFocus?: boolean; value?: string; onChange?: (value: string) => void; onFileBrowse?: () => void; sandboxId?: string; hideAttachments?: boolean; selectedAgentId?: string; onAgentSelect?: (agentId: string | undefined) => void; agentName?: string; messages?: any[]; bgColor?: string; toolCalls?: ToolCallInput[]; toolCallIndex?: number; showToolPreview?: boolean; onExpandToolPreview?: () => void; isLoggedIn?: boolean; enableAdvancedConfig?: boolean; onConfigureAgent?: (agentId: string) => void; hideAgentSelection?: boolean; defaultShowSnackbar?: 'tokens' | 'upgrade' | false; showToLowCreditUsers?: boolean; agentMetadata?: { is_suna_default?: boolean; }; showScrollToBottomIndicator?: boolean; onScrollToBottom?: () => void; } export interface UploadedFile { name: string; path: string; size: number; type: string; localUrl?: string; } export const ChatInput = forwardRef( ( { onSubmit, placeholder = 'Describe what you need help with...', loading = false, disabled = false, isAgentRunning = false, onStopAgent, autoFocus = true, value: controlledValue, onChange: controlledOnChange, onFileBrowse, sandboxId, hideAttachments = false, selectedAgentId, onAgentSelect, agentName, messages = [], bgColor = 'bg-card', toolCalls = [], toolCallIndex = 0, showToolPreview = false, onExpandToolPreview, isLoggedIn = true, enableAdvancedConfig = false, onConfigureAgent, hideAgentSelection = false, defaultShowSnackbar = false, showToLowCreditUsers = true, agentMetadata, showScrollToBottomIndicator = false, onScrollToBottom, }, ref, ) => { const isControlled = controlledValue !== undefined && controlledOnChange !== undefined; const router = useRouter(); const [uncontrolledValue, setUncontrolledValue] = useState(''); const value = isControlled ? controlledValue : uncontrolledValue; const isSunaAgent = agentMetadata?.is_suna_default || false; const [uploadedFiles, setUploadedFiles] = useState([]); const [pendingFiles, setPendingFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [isDraggingOver, setIsDraggingOver] = useState(false); const [registryDialogOpen, setRegistryDialogOpen] = useState(false); const [showSnackbar, setShowSnackbar] = useState(defaultShowSnackbar); const [userDismissedUsage, setUserDismissedUsage] = useState(false); const [billingModalOpen, setBillingModalOpen] = useState(false); const { selectedModel, setSelectedModel: handleModelChange, subscriptionStatus, allModels: modelOptions, canAccessModel, getActualModelId, refreshCustomModels, } = useModelSelection(); const { data: subscriptionData } = useSubscriptionData(); const deleteFileMutation = useFileDelete(); const queryClient = useQueryClient(); // Fetch integration icons only when logged in and advanced config UI is in use const shouldFetchIcons = isLoggedIn && !!enableAdvancedConfig; const { data: googleDriveIcon } = useComposioToolkitIcon('googledrive', { enabled: shouldFetchIcons }); const { data: slackIcon } = useComposioToolkitIcon('slack', { enabled: shouldFetchIcons }); const { data: notionIcon } = useComposioToolkitIcon('notion', { enabled: shouldFetchIcons }); // Show usage preview logic: // - Always show to free users when showToLowCreditUsers is true // - For paid users, only show when they're at 70% or more of their cost limit (30% or below remaining) const shouldShowUsage = !isLocalMode() && subscriptionData && showToLowCreditUsers && (() => { // Free users: always show if (subscriptionStatus === 'no_subscription') { return true; } // Paid users: only show when at 70% or more of cost limit const currentUsage = subscriptionData.current_usage || 0; const costLimit = subscriptionData.cost_limit || 0; if (costLimit === 0) return false; // No limit set return currentUsage >= (costLimit * 0.7); // 70% or more used (30% or less remaining) })(); // Auto-show usage preview when we have subscription data useEffect(() => { if (shouldShowUsage && defaultShowSnackbar !== false && !userDismissedUsage && (showSnackbar === false || showSnackbar === defaultShowSnackbar)) { setShowSnackbar('upgrade'); } else if (!shouldShowUsage && showSnackbar !== false) { setShowSnackbar(false); } }, [subscriptionData, showSnackbar, defaultShowSnackbar, shouldShowUsage, subscriptionStatus, showToLowCreditUsers, userDismissedUsage]); const textareaRef = useRef(null); const fileInputRef = useRef(null); const { data: agentsResponse } = useAgents({}, { enabled: isLoggedIn }); const agents = agentsResponse?.agents || []; const { initializeFromAgents } = useAgentSelection(); useImperativeHandle(ref, () => ({ getPendingFiles: () => pendingFiles, clearPendingFiles: () => setPendingFiles([]), })); useEffect(() => { if (agents.length > 0 && !onAgentSelect) { initializeFromAgents(agents); } }, [agents, onAgentSelect, initializeFromAgents]); useEffect(() => { if (autoFocus && textareaRef.current) { textareaRef.current.focus(); } }, [autoFocus]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if ( (!value.trim() && uploadedFiles.length === 0) || loading || (disabled && !isAgentRunning) ) return; if (isAgentRunning && onStopAgent) { onStopAgent(); return; } let message = value; if (uploadedFiles.length > 0) { const fileInfo = uploadedFiles .map((file) => `[Uploaded File: ${file.path}]`) .join('\n'); message = message ? `${message}\n\n${fileInfo}` : fileInfo; } let baseModelName = getActualModelId(selectedModel); let thinkingEnabled = false; if (selectedModel.endsWith('-thinking')) { baseModelName = getActualModelId(selectedModel.replace(/-thinking$/, '')); thinkingEnabled = true; } posthog.capture("task_prompt_submitted", { message }); onSubmit(message, { agent_id: selectedAgentId, model_name: baseModelName, enable_thinking: thinkingEnabled, }); if (!isControlled) { setUncontrolledValue(''); } setUploadedFiles([]); }; const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value; if (isControlled) { controlledOnChange(newValue); } else { setUncontrolledValue(newValue); } }; const handleTranscription = (transcribedText: string) => { const currentValue = isControlled ? controlledValue : uncontrolledValue; const newValue = currentValue ? `${currentValue} ${transcribedText}` : transcribedText; if (isControlled) { controlledOnChange(newValue); } else { setUncontrolledValue(newValue); } }; const removeUploadedFile = async (index: number) => { const fileToRemove = uploadedFiles[index]; // Clean up local URL if it exists if (fileToRemove.localUrl) { URL.revokeObjectURL(fileToRemove.localUrl); } // Remove from local state immediately for responsive UI setUploadedFiles((prev) => prev.filter((_, i) => i !== index)); if (!sandboxId && pendingFiles.length > index) { setPendingFiles((prev) => prev.filter((_, i) => i !== index)); } // Check if file is referenced in existing chat messages before deleting from server const isFileUsedInChat = messages.some(message => { const content = typeof message.content === 'string' ? message.content : ''; return content.includes(`[Uploaded File: ${fileToRemove.path}]`); }); // Only delete from server if file is not referenced in chat history if (sandboxId && fileToRemove.path && !isFileUsedInChat) { deleteFileMutation.mutate({ sandboxId, filePath: fileToRemove.path, }, { onError: (error) => { console.error('Failed to delete file from server:', error); } }); } else { // File exists in chat history, don't delete from server } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDraggingOver(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDraggingOver(false); }; return (
{ setShowSnackbar(false); setUserDismissedUsage(true); }} onOpenUpgrade={() => setBillingModalOpen(true)} isVisible={showToolPreview || !!showSnackbar} /> {/* Scroll to bottom button */} {showScrollToBottomIndicator && onScrollToBottom && ( )} { e.preventDefault(); e.stopPropagation(); setIsDraggingOver(false); if (fileInputRef.current && e.dataTransfer.files.length > 0) { const files = Array.from(e.dataTransfer.files); handleFiles( files, sandboxId, setPendingFiles, setUploadedFiles, setIsUploading, messages, queryClient, ); } }} >
{enableAdvancedConfig && selectedAgentId && (
)} Integrations { // Save to workflow or perform other action here }} />
); }, ); ChatInput.displayName = 'ChatInput';