suna/frontend/src/components/thread/chat-input/message-input.tsx

185 lines
5.6 KiB
TypeScript

import React, { forwardRef, useEffect } 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 { ModelSelector } from './model-selector';
import { useModelSelection } from './_use-model-selection';
interface MessageInputProps {
value: string;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onSubmit: (e: React.FormEvent) => 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;
selectedModel: string;
onModelChange: (model: string) => void;
modelOptions: any[];
currentTier: string;
canAccessModel: (model: string) => boolean;
}
export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
(
{
value,
onChange,
onSubmit,
placeholder,
loading,
disabled,
isAgentRunning,
onStopAgent,
isDraggingOver,
uploadedFiles,
fileInputRef,
isUploading,
sandboxId,
setPendingFiles,
setUploadedFiles,
setIsUploading,
hideAttachments = false,
selectedModel,
onModelChange,
modelOptions,
currentTier,
canAccessModel,
},
ref,
) => {
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();
// Call it twice to ensure proper height calculation
adjustHeight();
window.addEventListener('resize', adjustHeight);
return () => window.removeEventListener('resize', adjustHeight);
}, [value, ref]);
const {
subscriptionTier,
} = useModelSelection();
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (
(value.trim() || uploadedFiles.length > 0) &&
!loading &&
(!disabled || isAgentRunning)
) {
onSubmit(e as unknown as React.FormEvent);
}
}
};
return (
<div className="flex flex-col w-full h-auto gap-4 justify-between">
<div className="flex gap-2 items-center px-2">
<Textarea
ref={ref}
value={value}
onChange={onChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={cn(
'w-full bg-transparent dark:bg-transparent border-none shadow-none focus-visible:ring-0 px-2 py-1 text-base min-h-[40px] max-h-[200px] overflow-y-auto resize-none',
isDraggingOver ? 'opacity-40' : '',
)}
disabled={loading || (disabled && !isAgentRunning)}
rows={2}
/>
</div>
<div className="flex items-center justify-between mt-1 ml-3 mb-1 pr-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}
/>
)}
</div>
<div className='flex items-center gap-2'>
<ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
currentTier={subscriptionTier}
canAccessModel={canAccessModel}
/>
<Button
type="submit"
onClick={isAgentRunning && onStopAgent ? onStopAgent : onSubmit}
size="sm"
className={cn(
'w-7 h-7 flex-shrink-0 self-end',
isAgentRunning ? 'bg-red-500 hover:bg-red-600' : '',
(!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-4 w-4 animate-spin" />
) : isAgentRunning ? (
<Square className="h-4 w-4" />
) : (
<ArrowUp className="h-4 w-4" />
)}
</Button>
</div>
</div>
</div>
);
},
);
MessageInput.displayName = 'MessageInput';