|
|
import React, { forwardRef, useEffect, useState } from 'react'; |
|
|
import { Textarea } from '@/components/ui/textarea'; |
|
|
import { Button } from '@/components/ui/button'; |
|
|
import { Square, Loader2, ArrowUp } from 'lucide-react'; |
|
|
import { cn } from '@/lib/utils'; |
|
|
import { UploadedFile } from './chat-input'; |
|
|
import { FileUploadHandler } from './file-upload-handler'; |
|
|
import { VoiceRecorder } from './voice-recorder'; |
|
|
import { ModelSelector } from './model-selector'; |
|
|
import { AgentSelector } from './agent-selector'; |
|
|
import { canAccessModel, SubscriptionStatus } from './_use-model-selection'; |
|
|
import { isLocalMode } from '@/lib/config'; |
|
|
import { useFeatureFlag } from '@/lib/feature-flags'; |
|
|
import { TooltipContent } from '@/components/ui/tooltip'; |
|
|
import { Tooltip } from '@/components/ui/tooltip'; |
|
|
import { TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip'; |
|
|
import { BillingModal } from '@/components/billing/billing-modal'; |
|
|
import ChatDropdown from './chat-dropdown'; |
|
|
import { handleFiles } from './file-upload-handler'; |
|
|
|
|
|
interface MessageInputProps { |
|
|
value: string; |
|
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; |
|
|
onSubmit: (e: React.FormEvent) => void; |
|
|
onTranscription: (text: string) => void; |
|
|
placeholder: string; |
|
|
loading: boolean; |
|
|
disabled: boolean; |
|
|
isAgentRunning: boolean; |
|
|
onStopAgent?: () => void; |
|
|
isDraggingOver: boolean; |
|
|
uploadedFiles: UploadedFile[]; |
|
|
|
|
|
fileInputRef: React.RefObject<HTMLInputElement>; |
|
|
isUploading: boolean; |
|
|
sandboxId?: string; |
|
|
setPendingFiles: React.Dispatch<React.SetStateAction<File[]>>; |
|
|
setUploadedFiles: React.Dispatch<React.SetStateAction<UploadedFile[]>>; |
|
|
setIsUploading: React.Dispatch<React.SetStateAction<boolean>>; |
|
|
hideAttachments?: boolean; |
|
|
messages?: any[]; |
|
|
isLoggedIn?: boolean; |
|
|
|
|
|
selectedModel: string; |
|
|
onModelChange: (model: string) => void; |
|
|
modelOptions: any[]; |
|
|
subscriptionStatus: SubscriptionStatus; |
|
|
canAccessModel: (modelId: string) => boolean; |
|
|
refreshCustomModels?: () => void; |
|
|
selectedAgentId?: string; |
|
|
onAgentSelect?: (agentId: string | undefined) => void; |
|
|
enableAdvancedConfig?: boolean; |
|
|
hideAgentSelection?: boolean; |
|
|
} |
|
|
|
|
|
export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>( |
|
|
( |
|
|
{ |
|
|
value, |
|
|
onChange, |
|
|
onSubmit, |
|
|
onTranscription, |
|
|
placeholder, |
|
|
loading, |
|
|
disabled, |
|
|
isAgentRunning, |
|
|
onStopAgent, |
|
|
isDraggingOver, |
|
|
uploadedFiles, |
|
|
|
|
|
fileInputRef, |
|
|
isUploading, |
|
|
sandboxId, |
|
|
setPendingFiles, |
|
|
setUploadedFiles, |
|
|
setIsUploading, |
|
|
hideAttachments = false, |
|
|
messages = [], |
|
|
isLoggedIn = true, |
|
|
|
|
|
selectedModel, |
|
|
onModelChange, |
|
|
modelOptions, |
|
|
subscriptionStatus, |
|
|
canAccessModel, |
|
|
refreshCustomModels, |
|
|
|
|
|
selectedAgentId, |
|
|
onAgentSelect, |
|
|
enableAdvancedConfig = false, |
|
|
hideAgentSelection = false, |
|
|
}, |
|
|
ref, |
|
|
) => { |
|
|
const [billingModalOpen, setBillingModalOpen] = useState(false); |
|
|
const { enabled: customAgentsEnabled, loading: flagsLoading } = useFeatureFlag('custom_agents'); |
|
|
|
|
|
useEffect(() => { |
|
|
const textarea = ref as React.RefObject<HTMLTextAreaElement>; |
|
|
if (!textarea.current) return; |
|
|
|
|
|
const adjustHeight = () => { |
|
|
textarea.current!.style.height = 'auto'; |
|
|
const newHeight = Math.min( |
|
|
Math.max(textarea.current!.scrollHeight, 24), |
|
|
200, |
|
|
); |
|
|
textarea.current!.style.height = `${newHeight}px`; |
|
|
}; |
|
|
|
|
|
adjustHeight(); |
|
|
|
|
|
|
|
|
adjustHeight(); |
|
|
|
|
|
window.addEventListener('resize', adjustHeight); |
|
|
return () => window.removeEventListener('resize', adjustHeight); |
|
|
}, [value, ref]); |
|
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { |
|
|
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { |
|
|
e.preventDefault(); |
|
|
if ( |
|
|
(value.trim() || uploadedFiles.length > 0) && |
|
|
!loading && |
|
|
(!disabled || isAgentRunning) |
|
|
) { |
|
|
onSubmit(e as unknown as React.FormEvent); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => { |
|
|
if (!e.clipboardData) return; |
|
|
const items = Array.from(e.clipboardData.items); |
|
|
const imageFiles: File[] = []; |
|
|
for (const item of items) { |
|
|
if (item.kind === 'file' && item.type.startsWith('image/')) { |
|
|
const file = item.getAsFile(); |
|
|
if (file) imageFiles.push(file); |
|
|
} |
|
|
} |
|
|
if (imageFiles.length > 0) { |
|
|
e.preventDefault(); |
|
|
handleFiles( |
|
|
imageFiles, |
|
|
sandboxId, |
|
|
setPendingFiles, |
|
|
setUploadedFiles, |
|
|
setIsUploading, |
|
|
messages, |
|
|
); |
|
|
} |
|
|
}; |
|
|
|
|
|
const renderDropdown = () => { |
|
|
if (isLoggedIn) { |
|
|
const showAdvancedFeatures = enableAdvancedConfig || (customAgentsEnabled && !flagsLoading); |
|
|
|
|
|
return ( |
|
|
<div className="flex items-center gap-2"> |
|
|
{showAdvancedFeatures && !hideAgentSelection && ( |
|
|
<AgentSelector |
|
|
selectedAgentId={selectedAgentId} |
|
|
onAgentSelect={onAgentSelect} |
|
|
disabled={loading || (disabled && !isAgentRunning)} |
|
|
/> |
|
|
)} |
|
|
<ModelSelector |
|
|
selectedModel={selectedModel} |
|
|
onModelChange={onModelChange} |
|
|
modelOptions={modelOptions} |
|
|
subscriptionStatus={subscriptionStatus} |
|
|
canAccessModel={canAccessModel} |
|
|
refreshCustomModels={refreshCustomModels} |
|
|
billingModalOpen={billingModalOpen} |
|
|
setBillingModalOpen={setBillingModalOpen} |
|
|
/> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
return <ChatDropdown />; |
|
|
} |
|
|
|
|
|
return ( |
|
|
<div className="relative flex flex-col w-full h-full gap-2 justify-between"> |
|
|
|
|
|
<div className="flex flex-col gap-1 px-2"> |
|
|
<Textarea |
|
|
ref={ref} |
|
|
value={value} |
|
|
onChange={onChange} |
|
|
onKeyDown={handleKeyDown} |
|
|
onPaste={handlePaste} |
|
|
placeholder={placeholder} |
|
|
className={cn( |
|
|
'w-full bg-transparent dark:bg-transparent border-none shadow-none focus-visible:ring-0 px-0.5 pb-6 pt-4 !text-[15px] min-h-[36px] max-h-[200px] overflow-y-auto resize-none', |
|
|
isDraggingOver ? 'opacity-40' : '', |
|
|
)} |
|
|
disabled={loading || (disabled && !isAgentRunning)} |
|
|
rows={1} |
|
|
/> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between mt-0 mb-1 px-2"> |
|
|
<div className="flex items-center gap-3"> |
|
|
{!hideAttachments && ( |
|
|
<FileUploadHandler |
|
|
ref={fileInputRef} |
|
|
loading={loading} |
|
|
disabled={disabled} |
|
|
isAgentRunning={isAgentRunning} |
|
|
isUploading={isUploading} |
|
|
sandboxId={sandboxId} |
|
|
setPendingFiles={setPendingFiles} |
|
|
setUploadedFiles={setUploadedFiles} |
|
|
setIsUploading={setIsUploading} |
|
|
messages={messages} |
|
|
isLoggedIn={isLoggedIn} |
|
|
/> |
|
|
)} |
|
|
|
|
|
</div> |
|
|
|
|
|
{subscriptionStatus === 'no_subscription' && !isLocalMode() && |
|
|
<TooltipProvider> |
|
|
<Tooltip> |
|
|
<TooltipTrigger> |
|
|
<p role='button' className='text-sm text-amber-500 hidden sm:block cursor-pointer' onClick={() => setBillingModalOpen(true)}>Upgrade for more usage</p> |
|
|
</TooltipTrigger> |
|
|
<TooltipContent> |
|
|
<p>The free tier is severely limited by the amount of usage. Upgrade to experience the full power of Suna.</p> |
|
|
</TooltipContent> |
|
|
</Tooltip> |
|
|
</TooltipProvider> |
|
|
} |
|
|
|
|
|
<div className='flex items-center gap-2'> |
|
|
{renderDropdown()} |
|
|
<BillingModal |
|
|
open={billingModalOpen} |
|
|
onOpenChange={setBillingModalOpen} |
|
|
returnUrl={typeof window !== 'undefined' ? window.location.href : '/'} |
|
|
/> |
|
|
|
|
|
{isLoggedIn && <VoiceRecorder |
|
|
onTranscription={onTranscription} |
|
|
disabled={loading || (disabled && !isAgentRunning)} |
|
|
/>} |
|
|
|
|
|
<Button |
|
|
type="submit" |
|
|
onClick={isAgentRunning && onStopAgent ? onStopAgent : onSubmit} |
|
|
size="sm" |
|
|
className={cn( |
|
|
'w-8 h-8 flex-shrink-0 self-end rounded-xl', |
|
|
(!value.trim() && uploadedFiles.length === 0 && !isAgentRunning) || |
|
|
loading || |
|
|
(disabled && !isAgentRunning) |
|
|
? 'opacity-50' |
|
|
: '', |
|
|
)} |
|
|
disabled={ |
|
|
(!value.trim() && uploadedFiles.length === 0 && !isAgentRunning) || |
|
|
loading || |
|
|
(disabled && !isAgentRunning) |
|
|
} |
|
|
> |
|
|
{loading ? ( |
|
|
<Loader2 className="h-5 w-5 animate-spin" /> |
|
|
) : isAgentRunning ? ( |
|
|
<div className="min-h-[14px] min-w-[14px] w-[14px] h-[14px] rounded-sm bg-current" /> |
|
|
) : ( |
|
|
<ArrowUp className="h-5 w-5" /> |
|
|
)} |
|
|
</Button> |
|
|
</div> |
|
|
</div> |
|
|
{subscriptionStatus === 'no_subscription' && !isLocalMode() && |
|
|
<div className='sm:hidden absolute -bottom-8 left-0 right-0 flex justify-center'> |
|
|
<p className='text-xs text-amber-500 px-2 py-1'> |
|
|
Upgrade for better performance |
|
|
</p> |
|
|
</div> |
|
|
} |
|
|
</div> |
|
|
); |
|
|
}, |
|
|
); |
|
|
|
|
|
MessageInput.displayName = 'MessageInput'; |