-
+
{!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 (
-