'use client' import type React from 'react' import { useCallback, useLayoutEffect, useRef, useState } from 'react' import { Badge, Button, cn, handleKeyboardActivation, Tooltip } from '@sim/emcn' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { ArrowUp, Mic, Paperclip, X } from 'lucide-react' import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation' import { VoiceInput } from '@/app/(interfaces)/chat/components/input/voice-input' const logger = createLogger('ChatInput') const MAX_TEXTAREA_HEIGHT = 200 interface AttachedFile { id: string name: string size: number type: string file: File dataUrl?: string } export const ChatInput: React.FC<{ onSubmit?: (value: string, isVoiceInput?: boolean, files?: AttachedFile[]) => void isStreaming?: boolean onStopStreaming?: () => void onVoiceStart?: () => void voiceOnly?: boolean sttAvailable?: boolean }> = ({ onSubmit, isStreaming = false, onStopStreaming, onVoiceStart, voiceOnly = false, sttAvailable = false, }) => { const fileInputRef = useRef(null) const textareaRef = useRef(null) const [inputValue, setInputValue] = useState('') const [attachedFiles, setAttachedFiles] = useState([]) const [uploadErrors, setUploadErrors] = useState([]) const [dragCounter, setDragCounter] = useState(0) const isDragOver = dragCounter > 0 useLayoutEffect(() => { const el = textareaRef.current if (!el) return el.style.height = 'auto' el.style.height = `${Math.min(el.scrollHeight, MAX_TEXTAREA_HEIGHT)}px` }, [inputValue]) const handleFileSelect = async (selectedFiles: FileList | null) => { if (!selectedFiles) return const newFiles: AttachedFile[] = [] const maxSize = 10 * 1024 * 1024 const maxFiles = 15 for (let i = 0; i < selectedFiles.length; i++) { if (attachedFiles.length + newFiles.length >= maxFiles) break const file = selectedFiles[i] if (file.size > maxSize) { setUploadErrors((prev) => [...prev, `${file.name} is too large (max 10MB)`]) continue } const isDuplicate = attachedFiles.some( (existing) => existing.name === file.name && existing.size === file.size ) if (isDuplicate) { setUploadErrors((prev) => [...prev, `${file.name} already added`]) continue } let dataUrl: string | undefined if (file.type.startsWith('image/')) { try { dataUrl = await new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result as string) reader.onerror = reject reader.readAsDataURL(file) }) } catch (error) { logger.error('Error reading file:', error) } } newFiles.push({ id: generateId(), name: file.name, size: file.size, type: file.type, file, dataUrl, }) } if (newFiles.length > 0) { setAttachedFiles((prev) => [...prev, ...newFiles]) setUploadErrors([]) } } const handleRemoveFile = useCallback((fileId: string) => { setAttachedFiles((prev) => prev.filter((f) => f.id !== fileId)) }, []) const handleSubmit = useCallback(() => { if (isStreaming) return if (!inputValue.trim() && attachedFiles.length === 0) return onSubmit?.(inputValue.trim(), false, attachedFiles) setInputValue('') setAttachedFiles([]) setUploadErrors([]) }, [isStreaming, inputValue, attachedFiles, onSubmit]) const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault() handleSubmit() } }, [handleSubmit] ) const focusTextarea = useCallback(() => { textareaRef.current?.focus() }, []) const handleContainerClick = useCallback((e: React.MouseEvent) => { if ((e.target as HTMLElement).closest('button')) return textareaRef.current?.focus() }, []) const canSubmit = (inputValue.trim().length > 0 || attachedFiles.length > 0) && !isStreaming if (voiceOnly) { return (
{sttAvailable && (
{})} disabled={isStreaming} large={true} />

Start voice conversation

)}
) } return (
{/* Error Messages */} {uploadErrors.length > 0 && (
{uploadErrors.map((error, idx) => ( {error} ))}
)} {/* Input container */}
{ if (event.target !== event.currentTarget) return handleKeyboardActivation(event, focusTextarea) }} className={cn( 'relative z-10 cursor-text rounded-2xl border border-[var(--border-1)] bg-[var(--surface-2)] px-2.5 py-2', isDragOver && 'border-purple-500' )} onDragEnter={(e) => { e.preventDefault() e.stopPropagation() if (!isStreaming) setDragCounter((prev) => prev + 1) }} onDragOver={(e) => { e.preventDefault() e.stopPropagation() if (!isStreaming) e.dataTransfer.dropEffect = 'copy' }} onDragLeave={(e) => { e.preventDefault() e.stopPropagation() setDragCounter((prev) => Math.max(0, prev - 1)) }} onDrop={(e) => { e.preventDefault() e.stopPropagation() setDragCounter(0) if (!isStreaming) handleFileSelect(e.dataTransfer.files) }} > {/* File thumbnails */} {attachedFiles.length > 0 && (
{attachedFiles.map((file) => (
{file.dataUrl ? ( {file.name} ) : (
{file.name.split('.').pop()}
)}

{file.name}

))}
)} {/* Textarea */}