/* eslint-disable @next/next/no-img-element */ // /components/ui/form-component.tsx import React, { useState, useRef, useCallback } from 'react'; import { motion } from 'framer-motion'; import { ChatRequestOptions, CreateMessage, Message } from 'ai'; 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 { X, Zap, ChevronDown, ScanEye } 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'; interface ModelSwitcherProps { selectedModel: string; setSelectedModel: (value: string) => void; className?: string; } const models = [ { value: "grok-2-1212", label: "Grok 2.0", icon: Zap, description: "Most intelligent text model", color: "glossyblack", vision: false }, { value: "grok-2-vision-1212", icon: ScanEye, label: "Grok 2.0 Vision", description: "Most intelligent vision model", color: "offgray", 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 'glossyblack': return isSelected ? `${baseClasses} ${selectedClasses} !bg-[#2D2D2D] dark:!bg-[#333333] !text-white hover:!text-white hover:!bg-[#1a1a1a] dark:hover:!bg-[#444444]` : `${baseClasses} !text-[#4A4A4A] dark:!text-[#F0F0F0] hover:!text-white hover:!bg-[#1a1a1a] dark:hover:!bg-[#333333]`; case 'offgray': return isSelected ? `${baseClasses} ${selectedClasses} !bg-[#4B5457] dark:!bg-[#707677] !text-white hover:!text-white hover:!bg-[#707677] dark:hover:!bg-[#4B5457]` : `${baseClasses} !text-[#5C6366] dark:!text-[#D1D5D6] hover:!text-white hover:!bg-[#707677] dark:hover:!bg-[#4B5457]`; 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' }, }; 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 MIN_HEIGHT = 56; const MAX_HEIGHT = 400; const autoResizeInput = (target: HTMLTextAreaElement) => { if (!target) return; requestAnimationFrame(() => { target.style.height = 'auto'; // reset let newHeight = target.scrollHeight; newHeight = Math.min(Math.max(newHeight, MIN_HEIGHT), MAX_HEIGHT); target.style.height = `${newHeight}px`; target.style.overflowY = newHeight >= MAX_HEIGHT ? 'auto' : 'hidden'; }); }; 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} /> ))}
)}