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 {
|
interface Attachment {
|
||||||
name: string;
|
name: string;
|
||||||
contentType: string;
|
contentType: string;
|
||||||
@ -244,12 +242,22 @@ const HomeContent = () => {
|
|||||||
id: "1",
|
id: "1",
|
||||||
title: "Dark mode is here!",
|
title: "Dark mode is here!",
|
||||||
images: [
|
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:
|
content:
|
||||||
`## **Dark Mode**
|
`## **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 }) => {
|
const SuggestionCards: React.FC<{ selectedModel: string }> = ({ selectedModel }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 mt-4">
|
<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) => {
|
const handleModelChange = useCallback((newModel: string) => {
|
||||||
setSelectedModel(newModel);
|
setSelectedModel(newModel);
|
||||||
setSuggestedQuestions([]);
|
setSuggestedQuestions([]);
|
||||||
reload({ body: { model: newModel } });
|
reload({ body: { model: newModel } });
|
||||||
}, [reload]);
|
}, [reload]);
|
||||||
|
|
||||||
|
const resetSuggestedQuestions = useCallback(() => {
|
||||||
|
setSuggestedQuestions([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return (
|
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">
|
<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 />
|
<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 && (
|
{!hasSubmitted && (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
@ -1892,11 +1804,6 @@ The most requested feature is finally here! You can now toggle between light and
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!hasSubmitted && (
|
|
||||||
<div className="flex items-center justify-between !-mb-2">
|
|
||||||
<ModelSwitcher selectedModel={selectedModel} setSelectedModel={handleModelChange} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{!hasSubmitted && (
|
{!hasSubmitted && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -1918,6 +1825,9 @@ The most requested feature is finally here! You can now toggle between light and
|
|||||||
stop={stop}
|
stop={stop}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
append={append}
|
append={append}
|
||||||
|
selectedModel={selectedModel}
|
||||||
|
setSelectedModel={handleModelChange}
|
||||||
|
resetSuggestedQuestions={resetSuggestedQuestions}
|
||||||
/>
|
/>
|
||||||
<SuggestionCards selectedModel={selectedModel} />
|
<SuggestionCards selectedModel={selectedModel} />
|
||||||
</motion.div>
|
</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>
|
<h2 className="text-base font-semibold text-neutral-800 dark:text-neutral-200">Answer</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<ModelSwitcher
|
|
||||||
selectedModel={selectedModel}
|
|
||||||
setSelectedModel={handleModelChange}
|
|
||||||
className="!px-4 rounded-full"
|
|
||||||
/>
|
|
||||||
<CopyButton text={message.content} />
|
<CopyButton text={message.content} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2081,6 +1986,9 @@ The most requested feature is finally here! You can now toggle between light and
|
|||||||
stop={stop}
|
stop={stop}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
append={append}
|
append={append}
|
||||||
|
selectedModel={selectedModel}
|
||||||
|
setSelectedModel={handleModelChange}
|
||||||
|
resetSuggestedQuestions={resetSuggestedQuestions}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,13 +1,101 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
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 { ChatRequestOptions, CreateMessage, Message } from 'ai';
|
||||||
|
import { track } from '@vercel/analytics';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import useWindowSize from '@/hooks/use-window-size';
|
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 {
|
interface Attachment {
|
||||||
name: string;
|
name: string;
|
||||||
@ -98,6 +186,9 @@ interface FormComponentProps {
|
|||||||
message: Message | CreateMessage,
|
message: Message | CreateMessage,
|
||||||
chatRequestOptions?: ChatRequestOptions,
|
chatRequestOptions?: ChatRequestOptions,
|
||||||
) => Promise<string | null | undefined>;
|
) => 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 }) => {
|
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 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.8 }}
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
transition={{ duration: 0.2 }}
|
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 ? (
|
{isUploading ? (
|
||||||
<div className="w-10 h-10 flex items-center justify-center">
|
<div className="w-10 h-10 flex items-center justify-center">
|
||||||
@ -203,9 +294,13 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
fileInputRef,
|
fileInputRef,
|
||||||
inputRef,
|
inputRef,
|
||||||
stop
|
stop,
|
||||||
|
messages,
|
||||||
|
append,
|
||||||
|
selectedModel,
|
||||||
|
setSelectedModel,
|
||||||
|
resetSuggestedQuestions,
|
||||||
}) => {
|
}) => {
|
||||||
const [uploadingAttachments, setUploadingAttachments] = useState<UploadingAttachment[]>([]);
|
|
||||||
const [uploadQueue, setUploadQueue] = useState<Array<string>>([]);
|
const [uploadQueue, setUploadQueue] = useState<Array<string>>([]);
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
const postSubmitFileInputRef = useRef<HTMLInputElement>(null);
|
const postSubmitFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -253,6 +348,12 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
|
|
||||||
const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = Array.from(event.target.files || []);
|
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));
|
setUploadQueue(files.map((file) => file.name));
|
||||||
|
|
||||||
@ -265,11 +366,12 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading files!", error);
|
console.error("Error uploading files!", error);
|
||||||
|
toast.error("Failed to upload one or more files. Please try again.");
|
||||||
} finally {
|
} finally {
|
||||||
setUploadQueue([]);
|
setUploadQueue([]);
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
}
|
}
|
||||||
}, [setAttachments]);
|
}, [attachments, setAttachments]);
|
||||||
|
|
||||||
const removeAttachment = (index: number) => {
|
const removeAttachment = (index: number) => {
|
||||||
setAttachments(prev => prev.filter((_, i) => i !== index));
|
setAttachments(prev => prev.filter((_, i) => i !== index));
|
||||||
@ -281,56 +383,53 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
|
|
||||||
if (input.trim() || attachments.length > 0) {
|
if (input.trim() || attachments.length > 0) {
|
||||||
setHasSubmitted(true);
|
setHasSubmitted(true);
|
||||||
|
track("search input", {query: input.trim()})
|
||||||
|
|
||||||
handleSubmit(event, {
|
handleSubmit(event, {
|
||||||
experimental_attachments: attachments,
|
experimental_attachments: attachments,
|
||||||
});
|
});
|
||||||
|
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
setUploadingAttachments([]);
|
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error("Please enter a search query or attach an image.");
|
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(() => {
|
const submitForm = useCallback(() => {
|
||||||
onSubmit({ preventDefault: () => { }, stopPropagation: () => { } } as React.FormEvent<HTMLFormElement>);
|
onSubmit({ preventDefault: () => { }, stopPropagation: () => { } } as React.FormEvent<HTMLFormElement>);
|
||||||
|
resetSuggestedQuestions();
|
||||||
|
|
||||||
if (width && width > 768) {
|
if (width && width > 768) {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
}, [onSubmit, width, inputRef]);
|
}, [onSubmit, resetSuggestedQuestions, width, inputRef]);
|
||||||
|
|
||||||
const triggerFileInput = useCallback(() => {
|
const triggerFileInput = useCallback(() => {
|
||||||
|
if (attachments.length >= MAX_IMAGES) {
|
||||||
|
toast.error(`You can only attach up to ${MAX_IMAGES} images.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasSubmitted) {
|
if (hasSubmitted) {
|
||||||
postSubmitFileInputRef.current?.click();
|
postSubmitFileInputRef.current?.click();
|
||||||
} else {
|
} else {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
}
|
}
|
||||||
}, [hasSubmitted, fileInputRef]);
|
}, [attachments.length, hasSubmitted, fileInputRef]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full flex flex-col gap-4">
|
<div className={cn(
|
||||||
<input
|
"relative w-full flex flex-col gap-2 rounded-lg transition-all duration-300",
|
||||||
type="file"
|
attachments.length > 0 || uploadQueue.length > 0
|
||||||
className="hidden"
|
? "bg-gray-100/70 dark:bg-neutral-800 p-1"
|
||||||
ref={fileInputRef}
|
: "bg-transparent"
|
||||||
multiple
|
)}>
|
||||||
onChange={handleFileChange}
|
<input type="file" className="hidden" ref={fileInputRef} multiple onChange={handleFileChange} tabIndex={-1} />
|
||||||
tabIndex={-1}
|
<input type="file" className="hidden" ref={postSubmitFileInputRef} multiple onChange={handleFileChange} tabIndex={-1} />
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
className="hidden"
|
|
||||||
ref={postSubmitFileInputRef}
|
|
||||||
multiple
|
|
||||||
onChange={handleFileChange}
|
|
||||||
tabIndex={-1}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{(attachments.length > 0 || uploadQueue.length > 0) && (
|
{(attachments.length > 0 || uploadQueue.length > 0) && (
|
||||||
<div className="flex flex-row gap-2 overflow-x-auto py-2 max-h-32 z-10">
|
<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}
|
isUploading={false}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{uploadQueue.map((filename) => (
|
{uploadQueue.map((filename) => (
|
||||||
<AttachmentPreview
|
<AttachmentPreview
|
||||||
key={filename}
|
key={filename}
|
||||||
@ -359,60 +457,80 @@ const FormComponent: React.FC<FormComponentProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Textarea
|
<div className="relative">
|
||||||
ref={inputRef}
|
<Textarea
|
||||||
placeholder={hasSubmitted ? "Ask a new question..." : "Ask a question..."}
|
ref={inputRef}
|
||||||
value={input}
|
placeholder={hasSubmitted ? "Ask a new question..." : "Ask a question..."}
|
||||||
onChange={handleInput}
|
value={input}
|
||||||
className="min-h-[24px] overflow-hidden resize-none rounded-lg text-base bg-muted"
|
onChange={handleInput}
|
||||||
rows={3}
|
disabled={isLoading}
|
||||||
onKeyDown={(event) => {
|
className={cn(
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
"min-h-[48px] overflow-hidden resize-none rounded-lg text-base",
|
||||||
event.preventDefault();
|
"bg-neutral-100 dark:bg-neutral-900",
|
||||||
|
"text-neutral-900 dark:text-neutral-100",
|
||||||
if (isLoading) {
|
"border border-neutral-200 dark:border-neutral-700",
|
||||||
toast.error("Please wait for the model to finish its response!");
|
"focus:border-neutral-300 dark:focus:border-neutral-600",
|
||||||
} else {
|
"focus:ring-2 focus:ring-neutral-300 dark:focus:ring-neutral-600",
|
||||||
submitForm();
|
"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
|
<div className="absolute left-2 bottom-2">
|
||||||
className="rounded-full p-1.5 h-fit absolute bottom-2 right-10 m-0.5 dark:border-zinc-700"
|
<ModelSwitcher
|
||||||
onClick={(event) => {
|
selectedModel={selectedModel}
|
||||||
event.preventDefault();
|
setSelectedModel={setSelectedModel}
|
||||||
triggerFileInput();
|
/>
|
||||||
}}
|
</div>
|
||||||
variant="outline"
|
|
||||||
disabled={isLoading}
|
<div className="absolute right-2 bottom-2 flex items-center gap-2">
|
||||||
>
|
<Button
|
||||||
<PaperclipIcon size={14} />
|
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"
|
||||||
</Button>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user