From c90e72664b11e4e5dd52b4572487a898139d09c3 Mon Sep 17 00:00:00 2001 From: zaidmukaddam Date: Wed, 16 Oct 2024 00:04:34 +0530 Subject: [PATCH] New Input Bar and few UI fixes --- app/search/page.tsx | 383 +++------------------------- components/ui/form-component.tsx | 420 +++++++++++++++++++++++++++++++ hooks/use-window-size.tsx | 39 +++ 3 files changed, 496 insertions(+), 346 deletions(-) create mode 100644 components/ui/form-component.tsx create mode 100644 hooks/use-window-size.tsx diff --git a/app/search/page.tsx b/app/search/page.tsx index fbe3527..dde3bfd 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -19,7 +19,7 @@ import Marked, { ReactRenderer } from 'marked-react'; import { track } from '@vercel/analytics'; import { useSearchParams } from 'next/navigation'; import { useChat } from 'ai/react'; -import { ToolInvocation } from 'ai'; +import { ChatRequestOptions, CreateMessage, Message, ToolInvocation } from 'ai'; import { toast } from 'sonner'; import { motion, AnimatePresence } from 'framer-motion'; import Image from 'next/image'; @@ -122,6 +122,7 @@ import { TableRow, } from "@/components/ui/table"; import Autoplay from 'embla-carousel-autoplay'; +import FormComponent from '@/components/ui/form-component'; export const maxDuration = 60; @@ -155,13 +156,13 @@ const HomeContent = () => { const [editingMessageIndex, setEditingMessageIndex] = useState(-1); const [attachments, setAttachments] = useState([]); const fileInputRef = useRef(null); - const inputRef = useRef(null); + const inputRef = useRef(null); const { theme } = useTheme(); const [openChangelog, setOpenChangelog] = useState(false); - const { isLoading, input, messages, setInput, handleInputChange, append, handleSubmit, setMessages, reload } = useChat({ + const { isLoading, input, messages, setInput, handleInputChange, append, handleSubmit, setMessages, reload, stop } = useChat({ maxToolRoundtrips: selectedModel === 'mistral:pixtral-12b-2409' ? 1 : 2, body: { model: selectedModel, @@ -1746,342 +1747,18 @@ The most requested feature is finally here! You can now toggle between light and progress: number; } - interface AttachmentPreviewProps { - attachment: Attachment | UploadingAttachment; - onRemove: () => void; - isUploading: boolean; - } - const AttachmentPreview: React.FC = React.memo(({ attachment, onRemove, isUploading }) => { - const formatFileSize = (bytes: number): string => { - if (bytes < 1024) return bytes + ' bytes'; - else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; - else return (bytes / 1048576).toFixed(1) + ' MB'; - }; - - const isUploadingAttachment = (attachment: Attachment | UploadingAttachment): attachment is UploadingAttachment => { - return 'progress' in attachment; - }; - - return ( - - - - {isUploading ? ( -
- -
- ) : isUploadingAttachment(attachment) ? ( -
-
- - - - -
- {Math.round(attachment.progress * 100)}% -
-
-
- ) : ( - {`Preview - )} -
- {!isUploadingAttachment(attachment) && ( -

{attachment.name}

- )} -

- {isUploadingAttachment(attachment) - ? 'Uploading...' - : formatFileSize((attachment as Attachment).size)} -

-
- { e.stopPropagation(); onRemove(); }} - className="absolute -top-2 -right-2 p-0.5 m-0 rounded-full bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 shadow-sm hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors z-20" - > - - -
-
- {!isUploadingAttachment(attachment) && ( - - {`Full - - )} -
- ); - }); - - AttachmentPreview.displayName = 'AttachmentPreview'; - - interface FormComponentProps { - input: string; - setInput: (input: string) => void; - attachments: Attachment[]; - setAttachments: React.Dispatch>; - hasSubmitted: boolean; - setHasSubmitted: (value: boolean) => void; - isLoading: boolean; - handleSubmit: (event: React.FormEvent, options?: { experimental_attachments?: Attachment[] }) => void; - fileInputRef: React.RefObject; - inputRef: React.RefObject; - } - - const FormComponent: React.FC = ({ - input, - setInput, - attachments, - setAttachments, - hasSubmitted, - setHasSubmitted, - isLoading, - handleSubmit, - fileInputRef, - inputRef, - }) => { - const [uploadingAttachments, setUploadingAttachments] = useState([]); - - const uploadFile = async (file: File): Promise => { - const formData = new FormData(); - formData.append('file', file); - - const response = await fetch('/api/upload', { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - throw new Error('Failed to upload file'); - } - - return await response.json(); - }; - - const handleFileChange = async (event: React.ChangeEvent) => { - const selectedFiles = event.target.files; - if (selectedFiles) { - const imageFiles = Array.from(selectedFiles).filter(file => file.type.startsWith('image/')); - if (imageFiles.length > 0) { - if (imageFiles.length + attachments.length + uploadingAttachments.length > MAX_IMAGES) { - toast.error(`You can only attach up to ${MAX_IMAGES} images.`); - return; - } - - const newUploadingAttachments = imageFiles.map(file => ({ file, progress: 0 })); - setUploadingAttachments(prev => [...prev, ...newUploadingAttachments]); - - for (const file of imageFiles) { - try { - const uploadedFile = await uploadFile(file); - setAttachments(prev => [...prev, uploadedFile]); - setUploadingAttachments(prev => prev.filter(ua => ua.file !== file)); - } catch (error) { - console.error("Error uploading file:", error); - toast.error(`Failed to upload ${file.name}`); - setUploadingAttachments(prev => prev.filter(ua => ua.file !== file)); - } - } - } else { - toast.error("Please select image files only."); - } - } - }; - - const removeAttachment = (index: number) => { - setAttachments(prev => prev.filter((_, i) => i !== index)); - }; - - const removeUploadingAttachment = (index: number) => { - setUploadingAttachments(prev => prev.filter((_, i) => i !== index)); - }; - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, [inputRef]); - - const onSubmit = (event: React.FormEvent) => { - event.preventDefault(); - event.stopPropagation(); - - if (input.trim() || (selectedModel !== 'openai/o1-mini' && attachments.length > 0)) { - track("search enter", { query: input.trim() }); - setHasSubmitted(true); - - handleSubmit(event, { - experimental_attachments: attachments, - }); - - setAttachments([]); - setUploadingAttachments([]); - setSuggestedQuestions([]); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - } else { - toast.error("Please enter a search query or attach an image."); - } - }; - - - return ( - { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - onSubmit(e); - } - }} - onDrag={e => e.preventDefault()} - onDrop={e => { - e.preventDefault(); - handleFileChange({ target: { files: e.dataTransfer?.files } } as React.ChangeEvent); - }} - className={` - ${hasSubmitted ? 'fixed bottom-4 left-1/2 -translate-x-1/2 max-w-[90%] sm:max-w-2xl' : 'max-w-full'} - ${attachments.length > 0 || uploadingAttachments.length > 0 ? 'rounded-2xl' : 'rounded-xl'} - w-full - bg-background border border-input dark:border-neutral-700 - overflow-hidden mb-4 - transition-all duration-300 ease-in-out - z-50 - `} - > -
0 || uploadingAttachments.length > 0 ? 'p-2' : 'p-0')}> - - {selectedModel !== 'openai/o1-mini' && (attachments.length > 0 || uploadingAttachments.length > 0) && ( - - {uploadingAttachments.map((attachment, index) => ( - removeUploadingAttachment(index)} - isUploading={true} - /> - ))} - {attachments.map((attachment, index) => ( - removeAttachment(index)} - isUploading={false} - /> - ))} - - )} - -
- { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - onSubmit(e as any); - } - }} - /> - - - -
-
-
- ); - }; const SuggestionCards: React.FC<{ selectedModel: string }> = ({ selectedModel }) => { return ( -
-
+
+
{suggestionCards.map((card, index) => (