/* eslint-disable @next/next/no-img-element */ import React, { useState, useRef, useEffect, useCallback } from 'react'; 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 useWindowSize from '@/hooks/use-window-size'; 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: "anthropic:claude-3-5-sonnet-latest", 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; contentType: string; url: string; size: number; } const ArrowUpIcon = ({ size = 16 }: { size?: number }) => { return ( ); }; const StopIcon = ({ size = 16 }: { size?: number }) => { return ( ); }; const PaperclipIcon = ({ size = 16 }: { size?: number }) => { return ( ); }; const MAX_IMAGES = 3; interface FormComponentProps { input: string; setInput: (input: string) => void; attachments: Array; setAttachments: React.Dispatch>>; hasSubmitted: boolean; setHasSubmitted: (value: boolean) => void; isLoading: boolean; handleSubmit: ( event?: { preventDefault?: () => void; }, chatRequestOptions?: ChatRequestOptions, ) => void; fileInputRef: React.RefObject; inputRef: React.RefObject; stop: () => void; messages: Array; append: ( 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 }) => { 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" >
); }; interface UploadingAttachment { file: File; progress: number; } const FormComponent: React.FC = ({ input, setInput, attachments, setAttachments, hasSubmitted, setHasSubmitted, isLoading, handleSubmit, fileInputRef, inputRef, stop, messages, append, selectedModel, setSelectedModel, resetSuggestedQuestions, }) => { const [uploadQueue, setUploadQueue] = useState>([]); const { width } = useWindowSize(); const postSubmitFileInputRef = useRef(null); const adjustHeight = useCallback(() => { if (inputRef.current) { inputRef.current.style.height = "auto"; inputRef.current.style.height = `${inputRef.current.scrollHeight + 2}px`; } }, [inputRef]); useEffect(() => { if (inputRef.current) { adjustHeight(); } }, [adjustHeight, input, inputRef]); const handleInput = (event: React.ChangeEvent) => { setInput(event.target.value); adjustHeight(); }; const uploadFile = async (file: File): Promise => { const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload', { method: 'POST', body: formData, }); if (response.ok) { const data = await response.json(); return data; } else { throw new Error('Failed to upload file'); } } catch (error) { console.error("Error uploading file:", error); toast.error("Failed to upload file, please try again!"); throw error; } }; 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)); try { const uploadPromises = files.map((file) => uploadFile(file)); const uploadedAttachments = await Promise.all(uploadPromises); setAttachments((currentAttachments) => [ ...currentAttachments, ...uploadedAttachments, ]); } 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 = ''; } }, [attachments, setAttachments]); const removeAttachment = (index: number) => { setAttachments(prev => prev.filter((_, i) => i !== index)); }; const onSubmit = useCallback((event: React.FormEvent) => { event.preventDefault(); event.stopPropagation(); if (input.trim() || attachments.length > 0) { setHasSubmitted(true); track("search input", {query: input.trim()}) handleSubmit(event, { experimental_attachments: attachments, }); setAttachments([]); if (fileInputRef.current) { fileInputRef.current.value = ''; } } else { toast.error("Please enter a search query or attach an image."); } }, [input, attachments, setHasSubmitted, handleSubmit, setAttachments, fileInputRef]); const submitForm = useCallback(() => { onSubmit({ preventDefault: () => { }, stopPropagation: () => { } } as React.FormEvent); resetSuggestedQuestions(); if (width && width > 768) { inputRef.current?.focus(); } }, [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(); } }, [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) && (
{attachments.map((attachment, index) => ( removeAttachment(index)} isUploading={false} /> ))} {uploadQueue.map((filename) => ( { }} isUploading={true} /> ))}
)}