Refactor input bar UI
This commit is contained in:
parent
dc104ea3ca
commit
be141ead3a
@ -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 (
|
||||
<div className="flex gap-3 mt-4">
|
||||
@ -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<ModelSwitcherProps> = ({ 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 (
|
||||
<DropdownMenu onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 rounded-full transition-all duration-300 shadow-sm text-sm",
|
||||
getColorClasses(selectedModelData.color, true),
|
||||
"focus:outline-none focus:ring-none",
|
||||
"transform hover:scale-105 active:scale-95",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<selectedModelData.icon className="w-4 h-4" />
|
||||
<span className="font-medium">{selectedModelData.label}</span>
|
||||
<ChevronDown className={cn(
|
||||
"w-3 h-3 transition-transform duration-200",
|
||||
isOpen && "transform rotate-180"
|
||||
)} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[200px] p-1 !font-sans ml-2 sm:m-auto rounded-lg shadow-md bg-white dark:bg-neutral-800">
|
||||
{models.map((model) => (
|
||||
<DropdownMenuItem
|
||||
key={model.value}
|
||||
onSelect={() => 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.icon className={cn(
|
||||
"w-5 h-5 mt-0.5",
|
||||
selectedModel === model.value ? "text-white" : `text-${model.color}-500 dark:text-${model.color}-400`
|
||||
)} />
|
||||
<div>
|
||||
<div className={cn(
|
||||
"font-bold",
|
||||
selectedModel === model.value ? "text-white" : `text-${model.color}-700 dark:text-${model.color}-300`
|
||||
)}>
|
||||
{model.label}
|
||||
</div>
|
||||
<div className={cn(
|
||||
"text-xs",
|
||||
selectedModel === model.value ? "text-white/80" : `text-${model.color}-600 dark:text-${model.color}-400`
|
||||
)}>
|
||||
{model.description}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const handleModelChange = useCallback((newModel: string) => {
|
||||
setSelectedModel(newModel);
|
||||
setSuggestedQuestions([]);
|
||||
reload({ body: { model: newModel } });
|
||||
}, [reload]);
|
||||
|
||||
const resetSuggestedQuestions = useCallback(() => {
|
||||
setSuggestedQuestions([]);
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col font-sans items-center justify-center p-2 sm:p-4 bg-background text-foreground transition-all duration-500">
|
||||
<Navbar />
|
||||
|
||||
<div className={`w-full max-w-[90%] sm:max-w-2xl space-y-6 p-0 ${hasSubmitted ? 'mt-16 sm:mt-20' : 'mt-[20vh] sm:mt-[30vh]'}`}>
|
||||
<div className={`w-full max-w-[90%] sm:max-w-2xl space-y-6 p-0 ${hasSubmitted ? 'mt-16 sm:mt-20' : 'mt-[20vh] sm:mt-[25vh]'}`}>
|
||||
{!hasSubmitted && (
|
||||
<div className="text-center">
|
||||
<Badge
|
||||
@ -1892,11 +1804,6 @@ The most requested feature is finally here! You can now toggle between light and
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
{!hasSubmitted && (
|
||||
<div className="flex items-center justify-between !-mb-2">
|
||||
<ModelSwitcher selectedModel={selectedModel} setSelectedModel={handleModelChange} />
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{!hasSubmitted && (
|
||||
<motion.div
|
||||
@ -1918,6 +1825,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}
|
||||
/>
|
||||
<SuggestionCards selectedModel={selectedModel} />
|
||||
</motion.div>
|
||||
@ -2007,11 +1917,6 @@ The most requested feature is finally here! You can now toggle between light and
|
||||
<h2 className="text-base font-semibold text-neutral-800 dark:text-neutral-200">Answer</h2>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<ModelSwitcher
|
||||
selectedModel={selectedModel}
|
||||
setSelectedModel={handleModelChange}
|
||||
className="!px-4 rounded-full"
|
||||
/>
|
||||
<CopyButton text={message.content} />
|
||||
</div>
|
||||
</div>
|
||||
@ -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}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@ -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<ModelSwitcherProps> = ({ selectedModel, setSelectedModel, className }) => {
|
||||
const selectedModelData = models.find(model => model.value === selectedModel) || models[0];
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<DropdownMenu onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
"flex items-center justify-center w-8 h-8 rounded-full transition-all duration-300",
|
||||
getColorClasses(selectedModelData.color, true),
|
||||
"focus:outline-none focus:ring-2 focus:ring-opacity-50",
|
||||
`focus:ring-${selectedModelData.color}-500`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<selectedModelData.icon className="w-4 h-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[220px] p-1 !font-sans rounded-md shadow-md bg-white dark:bg-neutral-800 ml-4 sm:m-auto">
|
||||
{models.map((model) => (
|
||||
<DropdownMenuItem
|
||||
key={model.value}
|
||||
onSelect={() => 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.icon className={cn(
|
||||
"w-4 h-4 mt-0.5",
|
||||
selectedModel === model.value ? "text-white" : `text-${model.color}-500 dark:text-${model.color}-400`
|
||||
)} />
|
||||
<div>
|
||||
<div className="font-bold">{model.label}</div>
|
||||
<div className="text-xs opacity-70">{model.description}</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
interface Attachment {
|
||||
name: string;
|
||||
@ -98,6 +186,9 @@ interface FormComponentProps {
|
||||
message: Message | CreateMessage,
|
||||
chatRequestOptions?: ChatRequestOptions,
|
||||
) => Promise<string | null | undefined>;
|
||||
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 ? (
|
||||
<div className="w-10 h-10 flex items-center justify-center">
|
||||
@ -203,9 +294,13 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
||||
handleSubmit,
|
||||
fileInputRef,
|
||||
inputRef,
|
||||
stop
|
||||
stop,
|
||||
messages,
|
||||
append,
|
||||
selectedModel,
|
||||
setSelectedModel,
|
||||
resetSuggestedQuestions,
|
||||
}) => {
|
||||
const [uploadingAttachments, setUploadingAttachments] = useState<UploadingAttachment[]>([]);
|
||||
const [uploadQueue, setUploadQueue] = useState<Array<string>>([]);
|
||||
const { width } = useWindowSize();
|
||||
const postSubmitFileInputRef = useRef<HTMLInputElement>(null);
|
||||
@ -253,6 +348,12 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
||||
|
||||
const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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<FormComponentProps> = ({
|
||||
]);
|
||||
} 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<FormComponentProps> = ({
|
||||
|
||||
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<HTMLFormElement>);
|
||||
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 (
|
||||
<div className="relative w-full flex flex-col gap-4">
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
ref={postSubmitFileInputRef}
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div className={cn(
|
||||
"relative w-full flex flex-col gap-2 rounded-lg transition-all duration-300",
|
||||
attachments.length > 0 || uploadQueue.length > 0
|
||||
? "bg-gray-100/70 dark:bg-neutral-800 p-1"
|
||||
: "bg-transparent"
|
||||
)}>
|
||||
<input type="file" className="hidden" ref={fileInputRef} multiple onChange={handleFileChange} tabIndex={-1} />
|
||||
<input type="file" className="hidden" ref={postSubmitFileInputRef} multiple onChange={handleFileChange} tabIndex={-1} />
|
||||
|
||||
{(attachments.length > 0 || uploadQueue.length > 0) && (
|
||||
<div className="flex flex-row gap-2 overflow-x-auto py-2 max-h-32 z-10">
|
||||
@ -342,7 +441,6 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
||||
isUploading={false}
|
||||
/>
|
||||
))}
|
||||
|
||||
{uploadQueue.map((filename) => (
|
||||
<AttachmentPreview
|
||||
key={filename}
|
||||
@ -359,60 +457,80 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
placeholder={hasSubmitted ? "Ask a new question..." : "Ask a question..."}
|
||||
value={input}
|
||||
onChange={handleInput}
|
||||
className="min-h-[24px] overflow-hidden resize-none rounded-lg text-base bg-muted"
|
||||
rows={3}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
||||
if (isLoading) {
|
||||
toast.error("Please wait for the model to finish its response!");
|
||||
} else {
|
||||
submitForm();
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
placeholder={hasSubmitted ? "Ask a new question..." : "Ask a question..."}
|
||||
value={input}
|
||||
onChange={handleInput}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
"min-h-[48px] overflow-hidden resize-none rounded-lg text-base",
|
||||
"bg-neutral-100 dark:bg-neutral-900",
|
||||
"text-neutral-900 dark:text-neutral-100",
|
||||
"border border-neutral-200 dark:border-neutral-700",
|
||||
"focus:border-neutral-300 dark:focus:border-neutral-600",
|
||||
"focus:ring-2 focus:ring-neutral-300 dark:focus:ring-neutral-600",
|
||||
"pr-20 py-2"
|
||||
)}
|
||||
rows={3}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (isLoading) {
|
||||
toast.error("Please wait for the model to finish its response!");
|
||||
} else {
|
||||
submitForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<Button
|
||||
className="rounded-full p-1.5 h-fit absolute bottom-2 right-2 m-0.5"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
stop();
|
||||
}}
|
||||
>
|
||||
<StopIcon size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="rounded-full p-1.5 h-fit absolute bottom-2 right-2 m-0.5"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
submitForm();
|
||||
}}
|
||||
disabled={input.length === 0 && attachments.length === 0 || uploadQueue.length > 0}
|
||||
>
|
||||
<ArrowUpIcon size={14} />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="rounded-full p-1.5 h-fit absolute bottom-2 right-10 m-0.5 dark:border-zinc-700"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
triggerFileInput();
|
||||
}}
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<PaperclipIcon size={14} />
|
||||
</Button>
|
||||
<div className="absolute left-2 bottom-2">
|
||||
<ModelSwitcher
|
||||
selectedModel={selectedModel}
|
||||
setSelectedModel={setSelectedModel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute right-2 bottom-2 flex items-center gap-2">
|
||||
<Button
|
||||
className="rounded-full p-1.5 h-8 w-8 bg-white dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-300 dark:hover:bg-neutral-600"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
triggerFileInput();
|
||||
}}
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<PaperclipIcon size={14} />
|
||||
</Button>
|
||||
|
||||
{isLoading ? (
|
||||
<Button
|
||||
className="rounded-full p-1.5 h-8 w-8"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
stop();
|
||||
}}
|
||||
variant="destructive"
|
||||
>
|
||||
<StopIcon size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="rounded-full p-1.5 h-8 w-8 "
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
submitForm();
|
||||
}}
|
||||
disabled={input.length === 0 && attachments.length === 0 || uploadQueue.length > 0}
|
||||
>
|
||||
<ArrowUpIcon size={14} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user