'use client'; import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle, } from 'react'; 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'; import { useFileDelete } from '@/hooks/react-query/files'; import { useQueryClient } from '@tanstack/react-query'; import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview'; import { Settings2, Sparkles, Brain, ChevronRight, Zap, Workflow, Database, Wrench } from 'lucide-react'; import { FaGoogle, FaDiscord } from 'react-icons/fa'; import { SiNotion } from 'react-icons/si'; import { AgentConfigModal } from '@/components/agents/agent-config-modal'; import { PipedreamRegistry } from '@/components/agents/pipedream/pipedream-registry'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; export interface ChatInputHandles { getPendingFiles: () => File[]; clearPendingFiles: () => void; } export interface ChatInputProps { onSubmit: ( message: string, options?: { model_name?: string; enable_thinking?: boolean }, ) => 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; showTokenUsage?: boolean; } 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, showTokenUsage = false, }, ref, ) => { const isControlled = controlledValue !== undefined && controlledOnChange !== undefined; const [uncontrolledValue, setUncontrolledValue] = useState(''); const value = isControlled ? controlledValue : uncontrolledValue; const [uploadedFiles, setUploadedFiles] = useState([]); const [pendingFiles, setPendingFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [isDraggingOver, setIsDraggingOver] = useState(false); const [configModalOpen, setConfigModalOpen] = useState(false); const [configModalTab, setConfigModalTab] = useState('integrations'); const [registryDialogOpen, setRegistryDialogOpen] = useState(false); const { selectedModel, setSelectedModel: handleModelChange, subscriptionStatus, allModels: modelOptions, canAccessModel, getActualModelId, refreshCustomModels, } = useModelSelection(); const deleteFileMutation = useFileDelete(); const queryClient = useQueryClient(); const textareaRef = useRef(null); const fileInputRef = useRef(null); const hasLoadedFromLocalStorage = useRef(false); useImperativeHandle(ref, () => ({ getPendingFiles: () => pendingFiles, clearPendingFiles: () => setPendingFiles([]), })); useEffect(() => { if (typeof window !== 'undefined' && onAgentSelect && !hasLoadedFromLocalStorage.current) { const urlParams = new URLSearchParams(window.location.search); const hasAgentIdInUrl = urlParams.has('agent_id'); if (!selectedAgentId && !hasAgentIdInUrl) { const savedAgentId = localStorage.getItem('lastSelectedAgentId'); if (savedAgentId) { const agentIdToSelect = savedAgentId === 'suna' ? undefined : savedAgentId; console.log('Loading saved agent from localStorage:', savedAgentId); onAgentSelect(agentIdToSelect); } else { console.log('No saved agent found in localStorage'); } } else { console.log('Skipping localStorage load:', { hasSelectedAgent: !!selectedAgentId, hasAgentIdInUrl, selectedAgentId }); } hasLoadedFromLocalStorage.current = true; } }, [onAgentSelect, selectedAgentId]); // Keep selectedAgentId to check current state // Save selected agent to localStorage whenever it changes useEffect(() => { if (typeof window !== 'undefined') { // Use 'suna' as a special key for the default agent (undefined) const keyToStore = selectedAgentId === undefined ? 'suna' : selectedAgentId; console.log('Saving selected agent to localStorage:', keyToStore); localStorage.setItem('lastSelectedAgentId', keyToStore); } }, [selectedAgentId]); 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; } onSubmit(message, { 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 = (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 if (isFileUsedInChat) { console.log(`Skipping server deletion for ${fileToRemove.path} - file is referenced in chat history`); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDraggingOver(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDraggingOver(false); }; return (
{ })} agentName={agentName} isVisible={showToolPreview} /> { 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 { console.log('Tools selected:', { profileId, selectedTools, appName, appSlug }); }} />
); }, ); ChatInput.displayName = 'ChatInput';