From be141ead3abc0fed23535da6da426fb048b0b8e7 Mon Sep 17 00:00:00 2001 From: zaidmukaddam Date: Wed, 16 Oct 2024 23:36:53 +0530 Subject: [PATCH] Refactor input bar UI --- app/search/page.tsx | 140 +++------------- components/ui/form-component.tsx | 278 ++++++++++++++++++++++--------- 2 files changed, 222 insertions(+), 196 deletions(-) diff --git a/app/search/page.tsx b/app/search/page.tsx index dde3bfd..3a30cbb 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -133,8 +133,6 @@ declare global { } } -const MAX_IMAGES = 3; - interface Attachment { name: string; contentType: string; @@ -244,12 +242,22 @@ const HomeContent = () => { id: "1", title: "Dark mode is here!", images: [ - "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-dark-mode.png", + "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-dark-mode-promo.png", + "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-new-input-bar-promo.png", + "https://metwm7frkvew6tn1.public.blob.vercel-storage.com/mplx-changelogs/mplx-gpt-4o-back-Lwzx44RD4XofYLAmrEsLD3Fngnn33K.png" ], content: `## **Dark Mode** -The most requested feature is finally here! You can now toggle between light and dark mode. Default is set to your system preference.`, +The most requested feature is finally here! You can now toggle between light and dark mode. Default is set to your system preference. + +## **New Input Bar Design** + +The input bar has been redesigned to make it more focused, user-friendly and accessible. The model selection dropdown has been moved to the bottom left corner inside the input bar. + +## **GPT-4o is back!** + +GPT-4o has been re-enabled! You can use it by selecting the model from the dropdown.`, } ]; @@ -1742,14 +1750,6 @@ The most requested feature is finally here! You can now toggle between light and ); }; - interface UploadingAttachment { - file: File; - progress: number; - } - - - - const SuggestionCards: React.FC<{ selectedModel: string }> = ({ selectedModel }) => { return (
@@ -1773,110 +1773,22 @@ The most requested feature is finally here! You can now toggle between light and ); }; - const models = [ - { value: "azure:gpt4o-mini", label: "OpenAI", icon: Zap, description: "High speed, lower quality", color: "emerald" }, - { value: "anthropicVertex:claude-3-5-sonnet@20240620", label: "Claude", icon: Sparkles, description: "High quality, lower speed", color: "indigo" }, - ] - - interface ModelSwitcherProps { - selectedModel: string; - setSelectedModel: (value: string) => void; - className?: string; - } - - const ModelSwitcher: React.FC = ({ selectedModel, setSelectedModel, className }) => { - const selectedModelData = models.find(model => model.value === selectedModel) || models[0]; - const [isOpen, setIsOpen] = useState(false); - - const getColorClasses = (color: string, isSelected: boolean = false) => { - switch (color) { - case 'emerald': - return isSelected - ? '!bg-emerald-500 dark:!bg-emerald-700 !text-white hover:!bg-emerald-600 dark:hover:!bg-emerald-800' - : '!text-emerald-700 dark:!text-emerald-300 hover:!bg-emerald-100 dark:hover:!bg-emerald-800/30'; - case 'indigo': - return isSelected - ? '!bg-indigo-500 dark:!bg-indigo-700 !text-white hover:!bg-indigo-600 dark:hover:!bg-indigo-800' - : '!text-indigo-700 dark:!text-indigo-300 hover:!bg-indigo-100 dark:hover:!bg-indigo-800/30'; - case 'blue': - return isSelected - ? '!bg-blue-500 dark:!bg-blue-700 !text-white hover:!bg-blue-600 dark:hover:!bg-blue-800' - : '!text-blue-700 dark:!text-blue-300 hover:!bg-blue-100 dark:hover:!bg-blue-800/30'; - default: - return isSelected - ? 'bg-neutral-500 dark:bg-neutral-600 text-white hover:bg-neutral-600 dark:hover:bg-neutral-700' - : 'text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800/30'; - } - } - - return ( - - - - {selectedModelData.label} - - - - {models.map((model) => ( - setSelectedModel(model.value)} - className={cn( - "flex items-start gap-2 px-2 py-1.5 rounded-lg text-sm mb-1 last:mb-0", - "transition-colors duration-200", - getColorClasses(model.color, selectedModel === model.value), - selectedModel === model.value && "hover:opacity-90" - )} - > - -
-
- {model.label} -
-
- {model.description} -
-
-
- ))} -
-
- ) - } - const handleModelChange = useCallback((newModel: string) => { setSelectedModel(newModel); setSuggestedQuestions([]); reload({ body: { model: newModel } }); }, [reload]); + const resetSuggestedQuestions = useCallback(() => { + setSuggestedQuestions([]); + }, []); + + return (
-
+
{!hasSubmitted && (
)} - {!hasSubmitted && ( -
- -
- )} {!hasSubmitted && ( @@ -2007,11 +1917,6 @@ The most requested feature is finally here! You can now toggle between light and

Answer

-
@@ -2081,6 +1986,9 @@ The most requested feature is finally here! You can now toggle between light and stop={stop} messages={messages} append={append} + selectedModel={selectedModel} + setSelectedModel={handleModelChange} + resetSuggestedQuestions={resetSuggestedQuestions} /> )} diff --git a/components/ui/form-component.tsx b/components/ui/form-component.tsx index db9950e..3305e01 100644 --- a/components/ui/form-component.tsx +++ b/components/ui/form-component.tsx @@ -1,13 +1,101 @@ /* eslint-disable @next/next/no-img-element */ import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { motion } from 'framer-motion'; import { ChatRequestOptions, CreateMessage, Message } from 'ai'; +import { track } from '@vercel/analytics'; import { toast } from 'sonner'; import { Button } from '../ui/button'; import { Textarea } from '../ui/textarea'; -import { cn } from '@/lib/utils'; import useWindowSize from '@/hooks/use-window-size'; -import { X } from 'lucide-react'; +import { Sparkles, X, Zap, Cpu } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { cn } from '@/lib/utils'; + +interface ModelSwitcherProps { + selectedModel: string; + setSelectedModel: (value: string) => void; + className?: string; +} + +const models = [ + { value: "azure:gpt4o-mini", label: "GPT-4o Mini", icon: Zap, description: "High speed, good quality", color: "emerald" }, + { value: "anthropicVertex:claude-3-5-sonnet@20240620", label: "Claude", icon: Sparkles, description: "High quality, lower speed", color: "indigo" }, + { value: "azure:gpt-4o", label: "GPT-4o", icon: Cpu, description: "Higher quality, normal speed", color: "blue" }, +]; + + +const getColorClasses = (color: string, isSelected: boolean = false) => { + const baseClasses = "transition-colors duration-200"; + const selectedClasses = isSelected ? "!bg-opacity-90 dark:!bg-opacity-90" : ""; + + switch (color) { + case 'emerald': + return isSelected + ? `${baseClasses} ${selectedClasses} !bg-emerald-500 dark:!bg-emerald-600 !text-white hover:!bg-emerald-600 dark:hover:!bg-emerald-700` + : `${baseClasses} !text-emerald-700 dark:!text-emerald-300 hover:!bg-emerald-200 dark:hover:!bg-emerald-800/70`; + case 'indigo': + return isSelected + ? `${baseClasses} ${selectedClasses} !bg-indigo-500 dark:!bg-indigo-600 !text-white hover:!bg-indigo-600 dark:hover:!bg-indigo-700` + : `${baseClasses} !text-indigo-700 dark:!text-indigo-300 hover:!bg-indigo-200 dark:hover:!bg-indigo-800/70`; + case 'blue': + return isSelected + ? `${baseClasses} ${selectedClasses} !bg-blue-500 dark:!bg-blue-600 !text-white hover:!bg-blue-600 dark:hover:!bg-blue-700` + : `${baseClasses} !text-blue-700 dark:!text-blue-300 hover:!bg-blue-200 dark:hover:!bg-blue-800/70`; + default: + return isSelected + ? `${baseClasses} ${selectedClasses} !bg-neutral-500 dark:!bg-neutral-600 !text-white hover:!bg-neutral-600 dark:hover:!bg-neutral-700` + : `${baseClasses} !text-neutral-700 dark:!text-neutral-300 hover:!bg-neutral-200 dark:hover:!bg-neutral-800/70`; + } +} + + +const ModelSwitcher: React.FC = ({ selectedModel, setSelectedModel, className }) => { + const selectedModelData = models.find(model => model.value === selectedModel) || models[0]; + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + + + {models.map((model) => ( + setSelectedModel(model.value)} + className={cn( + "flex items-start gap-2 px-2 py-1.5 rounded-md text-xs mb-1 last:mb-0", + getColorClasses(model.color, selectedModel === model.value) + )} + > + +
+
{model.label}
+
{model.description}
+
+
+ ))} +
+
+ ) +} + interface Attachment { name: string; @@ -98,6 +186,9 @@ interface FormComponentProps { message: Message | CreateMessage, chatRequestOptions?: ChatRequestOptions, ) => Promise; + selectedModel: string; + setSelectedModel: (value: string) => void; + resetSuggestedQuestions: () => void; } const AttachmentPreview: React.FC<{ attachment: Attachment | UploadingAttachment, onRemove: () => void, isUploading: boolean }> = ({ attachment, onRemove, isUploading }) => { @@ -118,7 +209,7 @@ const AttachmentPreview: React.FC<{ attachment: Attachment | UploadingAttachment animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.2 }} - className="relative flex items-center bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg p-2 pr-8 gap-2 cursor-pointer shadow-sm flex-shrink-0" + className="relative flex items-center bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg p-2 pr-8 gap-2 shadow-sm flex-shrink-0" > {isUploading ? (
@@ -203,9 +294,13 @@ const FormComponent: React.FC = ({ handleSubmit, fileInputRef, inputRef, - stop + stop, + messages, + append, + selectedModel, + setSelectedModel, + resetSuggestedQuestions, }) => { - const [uploadingAttachments, setUploadingAttachments] = useState([]); const [uploadQueue, setUploadQueue] = useState>([]); const { width } = useWindowSize(); const postSubmitFileInputRef = useRef(null); @@ -253,6 +348,12 @@ const FormComponent: React.FC = ({ const handleFileChange = useCallback(async (event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); + const totalAttachments = attachments.length + files.length; + + if (totalAttachments > MAX_IMAGES) { + toast.error(`You can only attach up to ${MAX_IMAGES} images.`); + return; + } setUploadQueue(files.map((file) => file.name)); @@ -265,11 +366,12 @@ const FormComponent: React.FC = ({ ]); } catch (error) { console.error("Error uploading files!", error); + toast.error("Failed to upload one or more files. Please try again."); } finally { setUploadQueue([]); event.target.value = ''; } - }, [setAttachments]); + }, [attachments, setAttachments]); const removeAttachment = (index: number) => { setAttachments(prev => prev.filter((_, i) => i !== index)); @@ -281,56 +383,53 @@ const FormComponent: React.FC = ({ if (input.trim() || attachments.length > 0) { setHasSubmitted(true); + track("search input", {query: input.trim()}) handleSubmit(event, { experimental_attachments: attachments, }); setAttachments([]); - setUploadingAttachments([]); if (fileInputRef.current) { fileInputRef.current.value = ''; } } else { toast.error("Please enter a search query or attach an image."); } - }, [input, attachments, setHasSubmitted, handleSubmit, setAttachments, setUploadingAttachments, fileInputRef]); + }, [input, attachments, setHasSubmitted, handleSubmit, setAttachments, fileInputRef]); const submitForm = useCallback(() => { onSubmit({ preventDefault: () => { }, stopPropagation: () => { } } as React.FormEvent); + resetSuggestedQuestions(); if (width && width > 768) { inputRef.current?.focus(); } - }, [onSubmit, width, inputRef]); + }, [onSubmit, resetSuggestedQuestions, width, inputRef]); const triggerFileInput = useCallback(() => { + if (attachments.length >= MAX_IMAGES) { + toast.error(`You can only attach up to ${MAX_IMAGES} images.`); + return; + } + if (hasSubmitted) { postSubmitFileInputRef.current?.click(); } else { fileInputRef.current?.click(); } - }, [hasSubmitted, fileInputRef]); + }, [attachments.length, hasSubmitted, fileInputRef]); return ( -
- - +
0 || uploadQueue.length > 0 + ? "bg-gray-100/70 dark:bg-neutral-800 p-1" + : "bg-transparent" + )}> + + {(attachments.length > 0 || uploadQueue.length > 0) && (
@@ -342,7 +441,6 @@ const FormComponent: React.FC = ({ isUploading={false} /> ))} - {uploadQueue.map((filename) => ( = ({
)} -