/* eslint-disable @next/next/no-img-element */ // /components/ui/form-component.tsx 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 { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'; import useWindowSize from '@/hooks/use-window-size'; import { Sparkles, X, Zap, Cpu, Search, ChevronDown, Check, Atom } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { cn, SearchGroup, SearchGroupId, searchGroups } from '@/lib/utils'; import { useMediaQuery } from '@/hooks/use-media-query'; import { XLogo } from '@phosphor-icons/react'; interface ModelSwitcherProps { selectedModel: string; setSelectedModel: (value: string) => void; className?: string; } const models = [ { value: "azure:gpt4o-mini", label: "GPT-4o Mini", icon: Zap, description: "God speed, good quality", color: "emerald", vision: true }, { value: "anthropic:claude-3-5-haiku-20241022", label: "Claude 3.5 Haiku", icon: Sparkles, description: "Good quality, high speed", color: "orange", vision: false }, { value: "xai:grok-2-vision-1212", label: "Grok 2.0 Vision", icon: XLogo, description: "Good quality, normal speed", color: "glossyblack", vision: true }, { value: "anthropic:claude-3-5-sonnet-latest", label: "Claude 3.5 Sonnet (New)", icon: Sparkles, description: "High quality, good speed", color: "indigo", vision: true }, { value: "azure:gpt-4o", label: "GPT-4o", icon: Cpu, description: "Higher quality, normal speed", color: "blue", vision: true }, ]; 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`; case 'orange': return isSelected ? `${baseClasses} ${selectedClasses} !bg-orange-500 dark:!bg-orange-600 !text-white hover:!bg-orange-600 dark:hover:!bg-orange-700` : `${baseClasses} !text-orange-700 dark:!text-orange-300 hover:!bg-orange-200 dark:hover:!bg-orange-800/70`; case 'glossyblack': return isSelected ? `${baseClasses} ${selectedClasses} bg-gradient-to-br from-black to-neutral-800 !text-white shadow-inner` : `${baseClasses} !text-black dark:!text-white hover:!bg-black/10 dark:hover:!bg-black/40`; 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; const hasVisionSupport = (modelValue: string): boolean => { const selectedModel = models.find(model => model.value === modelValue); return selectedModel?.vision === true }; 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; } 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; lastSubmittedQueryRef: React.MutableRefObject; selectedGroup: SearchGroupId; setSelectedGroup: React.Dispatch>; } // Add this component either in a new file or in your form component interface GroupSelectorProps { selectedGroup: SearchGroupId; onGroupSelect: (group: SearchGroup) => void; } const themeColors: Record = { web: { bg: '!bg-white hover:!bg-cyan-50 dark:!bg-neutral-900/40 dark:hover:!bg-cyan-950/40', bgHover: 'hover:!border-cyan-200 dark:hover:!border-cyan-500/30', bgSelected: '!bg-cyan-50 dark:!bg-cyan-950/40 !border-cyan-500 dark:!border-cyan-400', text: '!text-cyan-600 dark:!text-cyan-400', description: '!text-neutral-600 dark:!text-neutral-500', focus: 'focus:!ring-cyan-500 dark:focus:!ring-cyan-400' }, academic: { bg: '!bg-white hover:!bg-violet-50 dark:!bg-neutral-900/40 dark:hover:!bg-violet-950/40', bgHover: 'hover:!border-violet-200 dark:hover:!border-violet-500/30', bgSelected: '!bg-violet-50 dark:!bg-violet-950/40 !border-violet-500 dark:!border-violet-400', text: '!text-violet-600 dark:!text-violet-400', description: '!text-neutral-600 dark:!text-neutral-500', focus: 'focus:!ring-violet-500 dark:focus:!ring-violet-400' }, youtube: { bg: '!bg-white hover:!bg-red-50 dark:!bg-neutral-900/40 dark:hover:!bg-red-950/40', bgHover: 'hover:!border-red-200 dark:hover:!border-red-500/30', bgSelected: '!bg-red-50 dark:!bg-red-950/40 !border-red-500 dark:!border-red-400', text: '!text-red-600 dark:!text-red-400', description: '!text-neutral-600 dark:!text-neutral-500', focus: 'focus:!ring-red-500 dark:focus:!ring-red-400' }, x: { bg: '!bg-white hover:!bg-neutral-50 dark:!bg-neutral-900/40 dark:hover:!bg-neutral-800/40', bgHover: 'hover:!border-neutral-300 dark:hover:!border-neutral-600/30', bgSelected: '!bg-neutral-50 dark:!bg-neutral-800/40 !border-neutral-500 dark:!border-neutral-400', text: '!text-neutral-900 dark:!text-neutral-100', description: '!text-neutral-600 dark:!text-neutral-500', focus: 'focus:!ring-neutral-500 dark:focus:!ring-neutral-400' }, shopping: { bg: '!bg-white hover:!bg-green-50 dark:!bg-neutral-900/40 dark:hover:!bg-green-950/40', bgHover: 'hover:!border-green-200 dark:hover:!border-green-500/30', bgSelected: '!bg-green-50 dark:!bg-green-950/40 !border-green-500 dark:!border-green-400', text: '!text-green-600 dark:!text-green-400', description: '!text-neutral-600 dark:!text-neutral-500', focus: 'focus:!ring-green-500 dark:focus:!ring-green-400' }, writing: { bg: '!bg-white hover:!bg-blue-50 dark:!bg-neutral-900/40 dark:hover:!bg-blue-950/40', bgHover: 'hover:!border-blue-200 dark:hover:!border-blue-500/30', bgSelected: '!bg-blue-50 dark:!bg-blue-950/40 !border-blue-500 dark:!border-blue-400', text: '!text-blue-600 dark:!text-blue-400', description: '!text-neutral-600 dark:!text-neutral-500', focus: 'focus:!ring-blue-500 dark:focus:!ring-blue-400' } }; const DrawerSelectionContent = ({ selectedGroup, onGroupSelect }: { selectedGroup: SearchGroupId, onGroupSelect: (group: SearchGroup) => void }) => (
{searchGroups.map((group) => { const Icon = group.icon; const isSelected = selectedGroup === group.id; const groupColors = themeColors[group.id]; return (
); })}
); const DropdownSelectionContent = ({ selectedGroup, onGroupSelect }: { selectedGroup: SearchGroupId, onGroupSelect: (group: SearchGroup) => void }) => (
{searchGroups.map((group) => { const Icon = group.icon; const isSelected = selectedGroup === group.id; const groupColors = themeColors[group.id]; return ( onGroupSelect(group)} className={cn( "flex flex-col gap-2 p-4 rounded-lg cursor-pointer font-sans group/item", "transition-all duration-200 relative overflow-hidden", !isSelected && "border dark:border-neutral-800 border-neutral-200", groupColors.bg, groupColors.bgHover, isSelected && cn( "ring-1 dark:ring-white/20 ring-black/10", "shadow-lg", groupColors.bgSelected ) )} >
{group.name}

{group.description}

); })}
); const TriggerContent = ({ selectedGroup, isOpen }: { selectedGroup: SearchGroupId, isOpen: boolean }) => { const selectedGroupDetails = searchGroups.find(g => g.id === selectedGroup); const Icon = selectedGroupDetails?.icon; const colors = themeColors[selectedGroup]; return (
{Icon && ( )} {selectedGroupDetails?.name}
); }; const GroupSelector = ({ selectedGroup, onGroupSelect }: GroupSelectorProps) => { const [isOpen, setIsOpen] = useState(false); const isDesktop = useMediaQuery("(min-width: 768px)"); const handleGroupSelection = (group: SearchGroup) => { onGroupSelect(group); setIsOpen(false); }; if (!isDesktop) { return ( Select Search Type
); } return ( ); }; const FormComponent: React.FC = ({ input, setInput, attachments, setAttachments, hasSubmitted, setHasSubmitted, isLoading, handleSubmit, fileInputRef, inputRef, stop, messages, append, selectedModel, setSelectedModel, resetSuggestedQuestions, lastSubmittedQueryRef, selectedGroup, setSelectedGroup, }) => { const [uploadQueue, setUploadQueue] = useState>([]); const { width } = useWindowSize(); const postSubmitFileInputRef = useRef(null); const [isFocused, setIsFocused] = useState(false); const autoResizeInput = (target: HTMLTextAreaElement) => { if (target) { target.style.height = 'auto'; // trigger recalculate scrollHeight let additionalLineHeight = 0; if (target.value) { additionalLineHeight = parseFloat(window.getComputedStyle(target).lineHeight); } target.style.height = `${target.scrollHeight + additionalLineHeight}px`; } }; const handleInput = (event: React.ChangeEvent) => { setInput(event.target.value); autoResizeInput(event.target); }; const handleFocus = () => { setIsFocused(true); }; const handleBlur = () => { setIsFocused(false); }; const handleGroupSelect = useCallback((group: SearchGroup) => { setSelectedGroup(group.id); setInput(''); resetSuggestedQuestions(); inputRef.current?.focus(); }, [setSelectedGroup, setInput, resetSuggestedQuestions, inputRef]); // Keep existing file upload and form submission logic... 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); lastSubmittedQueryRef.current = input.trim(); 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, lastSubmittedQueryRef]); 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) && (
{/* Existing attachment previews */} {attachments.map((attachment, index) => ( removeAttachment(index)} isUploading={false} /> ))} {uploadQueue.map((filename) => ( { }} isUploading={true} /> ))}
)}