'use client'; import { Attachment, ChatRequestOptions, CreateMessage, Message } from 'ai'; import cx from 'classnames'; import { motion } from 'framer-motion'; import React, { useRef, useEffect, useState, useCallback, Dispatch, SetStateAction, ChangeEvent, } from 'react'; import { toast } from 'sonner'; import { useLocalStorage, useWindowSize } from 'usehooks-ts'; import { sanitizeUIMessages } from '@/lib/utils'; import { ArrowUpIcon, PaperclipIcon, StopIcon } from './icons'; import { PreviewAttachment } from './preview-attachment'; import { Button } from '../ui/button'; import { Textarea } from '../ui/textarea'; const suggestedActions = [ { title: 'What is the weather', label: 'in San Francisco?', action: 'What is the weather in San Francisco?', }, { title: 'Help me draft an essay', label: 'about Silicon Valley', action: 'Help me draft an essay about Silicon Valley', }, ]; export function MultimodalInput({ input, setInput, isLoading, stop, attachments, setAttachments, messages, setMessages, append, handleSubmit, className, }: { input: string; setInput: (value: string) => void; isLoading: boolean; stop: () => void; attachments: Array; setAttachments: Dispatch>>; messages: Array; setMessages: Dispatch>>; append: ( message: Message | CreateMessage, chatRequestOptions?: ChatRequestOptions ) => Promise; handleSubmit: ( event?: { preventDefault?: () => void; }, chatRequestOptions?: ChatRequestOptions ) => void; className?: string; }) { const textareaRef = useRef(null); const { width } = useWindowSize(); useEffect(() => { if (textareaRef.current) { adjustHeight(); } }, []); const adjustHeight = () => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${textareaRef.current.scrollHeight + 2}px`; } }; const [localStorageInput, setLocalStorageInput] = useLocalStorage( 'input', '' ); useEffect(() => { if (textareaRef.current) { const domValue = textareaRef.current.value; // Prefer DOM value over localStorage to handle hydration const finalValue = domValue || localStorageInput || ''; setInput(finalValue); adjustHeight(); } // Only run once after hydration // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { setLocalStorageInput(input); }, [input, setLocalStorageInput]); const handleInput = (event: React.ChangeEvent) => { setInput(event.target.value); adjustHeight(); }; const fileInputRef = useRef(null); const [uploadQueue, setUploadQueue] = useState>([]); const submitForm = useCallback(() => { handleSubmit(undefined, { experimental_attachments: attachments, }); setAttachments([]); setLocalStorageInput(''); if (width && width > 768) { textareaRef.current?.focus(); } }, [attachments, handleSubmit, setAttachments, setLocalStorageInput, width]); const uploadFile = async (file: File) => { const formData = new FormData(); formData.append('file', file); try { const response = await fetch(`/api/files/upload`, { method: 'POST', body: formData, }); if (response.ok) { const data = await response.json(); const { url, pathname, contentType } = data; return { url, name: pathname, contentType: contentType, }; } else { const { error } = await response.json(); toast.error(error); } } catch (error) { toast.error('Failed to upload file, please try again!'); } }; const handleFileChange = useCallback( async (event: ChangeEvent) => { const files = Array.from(event.target.files || []); setUploadQueue(files.map((file) => file.name)); try { const uploadPromises = files.map((file) => uploadFile(file)); const uploadedAttachments = await Promise.all(uploadPromises); const successfullyUploadedAttachments = uploadedAttachments.filter( (attachment) => attachment !== undefined ); setAttachments((currentAttachments) => [ ...currentAttachments, ...successfullyUploadedAttachments, ]); } catch (error) { console.error('Error uploading files!', error); } finally { setUploadQueue([]); } }, [setAttachments] ); return (
{messages.length === 0 && attachments.length === 0 && uploadQueue.length === 0 && (
{suggestedActions.map((suggestedAction, index) => ( 1 ? 'hidden sm:block' : 'block'} > ))}
)} {(attachments.length > 0 || uploadQueue.length > 0) && (
{attachments.map((attachment) => ( ))} {uploadQueue.map((filename) => ( ))}
)}