Spaces:
Build error
Build error
| Hello menu i got task for you i ant to improve the UI and nature of my bot on site i use api key from hugging face and some free model there but i got a free key from Nvidia that has many models here are prompt from 21dev don't code yet i got some things to show you first You are given a task to integrate an existing React component in the codebase | |
| The codebase should support: | |
| - shadcn project structure | |
| - Tailwind CSS | |
| - Typescript | |
| If it doesn't, provide instructions on how to setup project via shadcn CLI, install Tailwind or Typescript. | |
| Determine the default path for components and styles. | |
| If default path for components is not /components/ui, provide instructions on why it's important to create this folder | |
| Copy-paste this component to /components/ui folder: | |
| ```tsx | |
| ai-prompt-box.tsx | |
| import React from "react"; | |
| import * as TooltipPrimitive from "@radix-ui/react-tooltip"; | |
| import * as DialogPrimitive from "@radix-ui/react-dialog"; | |
| import { ArrowUp, Paperclip, Square, X, StopCircle, Mic, Globe, BrainCog, FolderCode } from "lucide-react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| // Utility function for className merging | |
| const cn = (...classes: (string | undefined | null | false)[]) => classes.filter(Boolean).join(" "); | |
| // Embedded CSS for minimal custom styles | |
| const styles = ` | |
| *:focus-visible { | |
| outline-offset: 0 !important; | |
| --ring-offset: 0 !important; | |
| } | |
| textarea::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| textarea::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| textarea::-webkit-scrollbar-thumb { | |
| background-color: #444444; | |
| border-radius: 3px; | |
| } | |
| textarea::-webkit-scrollbar-thumb:hover { | |
| background-color: #555555; | |
| } | |
| `; | |
| // Inject styles into document | |
| const styleSheet = document.createElement("style"); | |
| styleSheet.innerText = styles; | |
| document.head.appendChild(styleSheet); | |
| // Textarea Component | |
| interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { | |
| className?: string; | |
| } | |
| const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => ( | |
| <textarea | |
| className={cn( | |
| "flex w-full rounded-md border-none bg-transparent px-3 py-2.5 text-base text-gray-100 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 min-h-[44px] resize-none scrollbar-thin scrollbar-thumb-[#444444] scrollbar-track-transparent hover:scrollbar-thumb-[#555555]", | |
| className | |
| )} | |
| ref={ref} | |
| rows={1} | |
| {...props} | |
| /> | |
| )); | |
| Textarea.displayName = "Textarea"; | |
| // Tooltip Components | |
| const TooltipProvider = TooltipPrimitive.Provider; | |
| const Tooltip = TooltipPrimitive.Root; | |
| const TooltipTrigger = TooltipPrimitive.Trigger; | |
| const TooltipContent = React.forwardRef< | |
| React.ElementRef<typeof TooltipPrimitive.Content>, | |
| React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> | |
| >(({ className, sideOffset = 4, ...props }, ref) => ( | |
| <TooltipPrimitive.Content | |
| ref={ref} | |
| sideOffset={sideOffset} | |
| className={cn( | |
| "z-50 overflow-hidden rounded-md border border-[#333333] bg-[#1F2023] px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | |
| className | |
| )} | |
| {...props} | |
| /> | |
| )); | |
| TooltipContent.displayName = TooltipPrimitive.Content.displayName; | |
| // Dialog Components | |
| const Dialog = DialogPrimitive.Root; | |
| const DialogPortal = DialogPrimitive.Portal; | |
| const DialogOverlay = React.forwardRef< | |
| React.ElementRef<typeof DialogPrimitive.Overlay>, | |
| React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> | |
| >(({ className, ...props }, ref) => ( | |
| <DialogPrimitive.Overlay | |
| ref={ref} | |
| className={cn( | |
| "fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", | |
| className | |
| )} | |
| {...props} | |
| /> | |
| )); | |
| DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; | |
| const DialogContent = React.forwardRef< | |
| React.ElementRef<typeof DialogPrimitive.Content>, | |
| React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> | |
| >(({ className, children, ...props }, ref) => ( | |
| <DialogPortal> | |
| <DialogOverlay /> | |
| <DialogPrimitive.Content | |
| ref={ref} | |
| className={cn( | |
| "fixed left-[50%] top-[50%] z-50 grid w-full max-w-[90vw] md:max-w-[800px] translate-x-[-50%] translate-y-[-50%] gap-4 border border-[#333333] bg-[#1F2023] p-0 shadow-xl duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-2xl", | |
| className | |
| )} | |
| {...props} | |
| > | |
| {children} | |
| <DialogPrimitive.Close className="absolute right-4 top-4 z-10 rounded-full bg-[#2E3033]/80 p-2 hover:bg-[#2E3033] transition-all"> | |
| <X className="h-5 w-5 text-gray-200 hover:text-white" /> | |
| <span className="sr-only">Close</span> | |
| </DialogPrimitive.Close> | |
| </DialogPrimitive.Content> | |
| </DialogPortal> | |
| )); | |
| DialogContent.displayName = DialogPrimitive.Content.displayName; | |
| const DialogTitle = React.forwardRef< | |
| React.ElementRef<typeof DialogPrimitive.Title>, | |
| React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> | |
| >(({ className, ...props }, ref) => ( | |
| <DialogPrimitive.Title | |
| ref={ref} | |
| className={cn("text-lg font-semibold leading-none tracking-tight text-gray-100", className)} | |
| {...props} | |
| /> | |
| )); | |
| DialogTitle.displayName = DialogPrimitive.Title.displayName; | |
| // Button Component | |
| interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { | |
| variant?: "default" | "outline" | "ghost"; | |
| size?: "default" | "sm" | "lg" | "icon"; | |
| } | |
| const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( | |
| ({ className, variant = "default", size = "default", ...props }, ref) => { | |
| const variantClasses = { | |
| default: "bg-white hover:bg-white/80 text-black", | |
| outline: "border border-[#444444] bg-transparent hover:bg-[#3A3A40]", | |
| ghost: "bg-transparent hover:bg-[#3A3A40]", | |
| }; | |
| const sizeClasses = { | |
| default: "h-10 px-4 py-2", | |
| sm: "h-8 px-3 text-sm", | |
| lg: "h-12 px-6", | |
| icon: "h-8 w-8 rounded-full aspect-[1/1]", | |
| }; | |
| return ( | |
| <button | |
| className={cn( | |
| "inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50", | |
| variantClasses[variant], | |
| sizeClasses[size], | |
| className | |
| )} | |
| ref={ref} | |
| {...props} | |
| /> | |
| ); | |
| } | |
| ); | |
| Button.displayName = "Button"; | |
| // VoiceRecorder Component | |
| interface VoiceRecorderProps { | |
| isRecording: boolean; | |
| onStartRecording: () => void; | |
| onStopRecording: (duration: number) => void; | |
| visualizerBars?: number; | |
| } | |
| const VoiceRecorder: React.FC<VoiceRecorderProps> = ({ | |
| isRecording, | |
| onStartRecording, | |
| onStopRecording, | |
| visualizerBars = 32, | |
| }) => { | |
| const [time, setTime] = React.useState(0); | |
| const timerRef = React.useRef<NodeJS.Timeout | null>(null); | |
| React.useEffect(() => { | |
| if (isRecording) { | |
| onStartRecording(); | |
| timerRef.current = setInterval(() => setTime((t) => t + 1), 1000); | |
| } else { | |
| if (timerRef.current) { | |
| clearInterval(timerRef.current); | |
| timerRef.current = null; | |
| } | |
| onStopRecording(time); | |
| setTime(0); | |
| } | |
| return () => { | |
| if (timerRef.current) clearInterval(timerRef.current); | |
| }; | |
| }, [isRecording, time, onStartRecording, onStopRecording]); | |
| const formatTime = (seconds: number) => { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; | |
| }; | |
| return ( | |
| <div | |
| className={cn( | |
| "flex flex-col items-center justify-center w-full transition-all duration-300 py-3", | |
| isRecording ? "opacity-100" : "opacity-0 h-0" | |
| )} | |
| > | |
| <div className="flex items-center gap-2 mb-3"> | |
| <div className="h-2 w-2 rounded-full bg-red-500 animate-pulse" /> | |
| <span className="font-mono text-sm text-white/80">{formatTime(time)}</span> | |
| </div> | |
| <div className="w-full h-10 flex items-center justify-center gap-0.5 px-4"> | |
| {[...Array(visualizerBars)].map((_, i) => ( | |
| <div | |
| key={i} | |
| className="w-0.5 rounded-full bg-white/50 animate-pulse" | |
| style={{ | |
| height: `${Math.max(15, Math.random() * 100)}%`, | |
| animationDelay: `${i * 0.05}s`, | |
| animationDuration: `${0.5 + Math.random() * 0.5}s`, | |
| }} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ImageViewDialog Component | |
| interface ImageViewDialogProps { | |
| imageUrl: string | null; | |
| onClose: () => void; | |
| } | |
| const ImageViewDialog: React.FC<ImageViewDialogProps> = ({ imageUrl, onClose }) => { | |
| if (!imageUrl) return null; | |
| return ( | |
| <Dialog open={!!imageUrl} onOpenChange={onClose}> | |
| <DialogContent className="p-0 border-none bg-transparent shadow-none max-w-[90vw] md:max-w-[800px]"> | |
| <DialogTitle className="sr-only">Image Preview</DialogTitle> | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| exit={{ opacity: 0, scale: 0.95 }} | |
| transition={{ duration: 0.2, ease: "easeOut" }} | |
| className="relative bg-[#1F2023] rounded-2xl overflow-hidden shadow-2xl" | |
| > | |
| <img | |
| src={imageUrl} | |
| alt="Full preview" | |
| className="w-full max-h-[80vh] object-contain rounded-2xl" | |
| /> | |
| </motion.div> | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| }; | |
| // PromptInput Context and Components | |
| interface PromptInputContextType { | |
| isLoading: boolean; | |
| value: string; | |
| setValue: (value: string) => void; | |
| maxHeight: number | string; | |
| onSubmit?: () => void; | |
| disabled?: boolean; | |
| } | |
| const PromptInputContext = React.createContext<PromptInputContextType>({ | |
| isLoading: false, | |
| value: "", | |
| setValue: () => {}, | |
| maxHeight: 240, | |
| onSubmit: undefined, | |
| disabled: false, | |
| }); | |
| function usePromptInput() { | |
| const context = React.useContext(PromptInputContext); | |
| if (!context) throw new Error("usePromptInput must be used within a PromptInput"); | |
| return context; | |
| } | |
| interface PromptInputProps { | |
| isLoading?: boolean; | |
| value?: string; | |
| onValueChange?: (value: string) => void; | |
| maxHeight?: number | string; | |
| onSubmit?: () => void; | |
| children: React.ReactNode; | |
| className?: string; | |
| disabled?: boolean; | |
| onDragOver?: (e: React.DragEvent) => void; | |
| onDragLeave?: (e: React.DragEvent) => void; | |
| onDrop?: (e: React.DragEvent) => void; | |
| } | |
| const PromptInput = React.forwardRef<HTMLDivElement, PromptInputProps>( | |
| ( | |
| { | |
| className, | |
| isLoading = false, | |
| maxHeight = 240, | |
| value, | |
| onValueChange, | |
| onSubmit, | |
| children, | |
| disabled = false, | |
| onDragOver, | |
| onDragLeave, | |
| onDrop, | |
| }, | |
| ref | |
| ) => { | |
| const [internalValue, setInternalValue] = React.useState(value || ""); | |
| const handleChange = (newValue: string) => { | |
| setInternalValue(newValue); | |
| onValueChange?.(newValue); | |
| }; | |
| return ( | |
| <TooltipProvider> | |
| <PromptInputContext.Provider | |
| value={{ | |
| isLoading, | |
| value: value ?? internalValue, | |
| setValue: onValueChange ?? handleChange, | |
| maxHeight, | |
| onSubmit, | |
| disabled, | |
| }} | |
| > | |
| <div | |
| ref={ref} | |
| className={cn( | |
| "rounded-3xl border border-[#444444] bg-[#1F2023] p-2 shadow-[0_8px_30px_rgba(0,0,0,0.24)] transition-all duration-300", | |
| isLoading && "border-red-500/70", | |
| className | |
| )} | |
| onDragOver={onDragOver} | |
| onDragLeave={onDragLeave} | |
| onDrop={onDrop} | |
| > | |
| {children} | |
| </div> | |
| </PromptInputContext.Provider> | |
| </TooltipProvider> | |
| ); | |
| } | |
| ); | |
| PromptInput.displayName = "PromptInput"; | |
| interface PromptInputTextareaProps { | |
| disableAutosize?: boolean; | |
| placeholder?: string; | |
| } | |
| const PromptInputTextarea: React.FC<PromptInputTextareaProps & React.ComponentProps<typeof Textarea>> = ({ | |
| className, | |
| onKeyDown, | |
| disableAutosize = false, | |
| placeholder, | |
| ...props | |
| }) => { | |
| const { value, setValue, maxHeight, onSubmit, disabled } = usePromptInput(); | |
| const textareaRef = React.useRef<HTMLTextAreaElement>(null); | |
| React.useEffect(() => { | |
| if (disableAutosize || !textareaRef.current) return; | |
| textareaRef.current.style.height = "auto"; | |
| textareaRef.current.style.height = | |
| typeof maxHeight === "number" | |
| ? `${Math.min(textareaRef.current.scrollHeight, maxHeight)}px` | |
| : `min(${textareaRef.current.scrollHeight}px, ${maxHeight})`; | |
| }, [value, maxHeight, disableAutosize]); | |
| const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | |
| if (e.key === "Enter" && !e.shiftKey) { | |
| e.preventDefault(); | |
| onSubmit?.(); | |
| } | |
| onKeyDown?.(e); | |
| }; | |
| return ( | |
| <Textarea | |
| ref={textareaRef} | |
| value={value} | |
| onChange={(e) => setValue(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| className={cn("text-base", className)} | |
| disabled={disabled} | |
| placeholder={placeholder} | |
| {...props} | |
| /> | |
| ); | |
| }; | |
| interface PromptInputActionsProps extends React.HTMLAttributes<HTMLDivElement> {} | |
| const PromptInputActions: React.FC<PromptInputActionsProps> = ({ children, className, ...props }) => ( | |
| <div className={cn("flex items-center gap-2", className)} {...props}> | |
| {children} | |
| </div> | |
| ); | |
| interface PromptInputActionProps extends React.ComponentProps<typeof Tooltip> { | |
| tooltip: React.ReactNode; | |
| children: React.ReactNode; | |
| side?: "top" | "bottom" | "left" | "right"; | |
| } | |
| const PromptInputAction: React.FC<PromptInputActionProps> = ({ | |
| tooltip, | |
| children, | |
| className, | |
| side = "top", | |
| ...props | |
| }) => { | |
| const { disabled } = usePromptInput(); | |
| return ( | |
| <Tooltip {...props}> | |
| <TooltipTrigger asChild disabled={disabled}> | |
| {children} | |
| </TooltipTrigger> | |
| <TooltipContent side={side} className={className}> | |
| {tooltip} | |
| </TooltipContent> | |
| </Tooltip> | |
| ); | |
| }; | |
| // Custom Divider Component | |
| const CustomDivider: React.FC = () => ( | |
| <div className="relative h-6 w-[1.5px] mx-1"> | |
| <div | |
| className="absolute inset-0 bg-gradient-to-t from-transparent via-[#9b87f5]/70 to-transparent rounded-full" | |
| style={{ | |
| clipPath: "polygon(0% 0%, 100% 0%, 100% 40%, 140% 50%, 100% 60%, 100% 100%, 0% 100%, 0% 60%, -40% 50%, 0% 40%)", | |
| }} | |
| /> | |
| </div> | |
| ); | |
| // Main PromptInputBox Component | |
| interface PromptInputBoxProps { | |
| onSend?: (message: string, files?: File[]) => void; | |
| isLoading?: boolean; | |
| placeholder?: string; | |
| className?: string; | |
| } | |
| export const PromptInputBox = React.forwardRef((props: PromptInputBoxProps, ref: React.Ref<HTMLDivElement>) => { | |
| const { onSend = () => {}, isLoading = false, placeholder = "Type your message here...", className } = props; | |
| const [input, setInput] = React.useState(""); | |
| const [files, setFiles] = React.useState<File[]>([]); | |
| const [filePreviews, setFilePreviews] = React.useState<{ [key: string]: string }>({}); | |
| const [selectedImage, setSelectedImage] = React.useState<string | null>(null); | |
| const [isRecording, setIsRecording] = React.useState(false); | |
| const [showSearch, setShowSearch] = React.useState(false); | |
| const [showThink, setShowThink] = React.useState(false); | |
| const [showCanvas, setShowCanvas] = React.useState(false); | |
| const uploadInputRef = React.useRef<HTMLInputElement>(null); | |
| const promptBoxRef = React.useRef<HTMLDivElement>(null); | |
| const handleToggleChange = (value: string) => { | |
| if (value === "search") { | |
| setShowSearch((prev) => !prev); | |
| setShowThink(false); | |
| } else if (value === "think") { | |
| setShowThink((prev) => !prev); | |
| setShowSearch(false); | |
| } | |
| }; | |
| const handleCanvasToggle = () => setShowCanvas((prev) => !prev); | |
| const isImageFile = (file: File) => file.type.startsWith("image/"); | |
| const processFile = (file: File) => { | |
| if (!isImageFile(file)) { | |
| console.log("Only image files are allowed"); | |
| return; | |
| } | |
| if (file.size > 10 * 1024 * 1024) { | |
| console.log("File too large (max 10MB)"); | |
| return; | |
| } | |
| setFiles([file]); | |
| const reader = new FileReader(); | |
| reader.onload = (e) => setFilePreviews({ [file.name]: e.target?.result as string }); | |
| reader.readAsDataURL(file); | |
| }; | |
| const handleDragOver = React.useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }, []); | |
| const handleDragLeave = React.useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }, []); | |
| const handleDrop = React.useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const files = Array.from(e.dataTransfer.files); | |
| const imageFiles = files.filter((file) => isImageFile(file)); | |
| if (imageFiles.length > 0) processFile(imageFiles[0]); | |
| }, []); | |
| const handleRemoveFile = (index: number) => { | |
| const fileToRemove = files[index]; | |
| if (fileToRemove && filePreviews[fileToRemove.name]) setFilePreviews({}); | |
| setFiles([]); | |
| }; | |
| const openImageModal = (imageUrl: string) => setSelectedImage(imageUrl); | |
| const handlePaste = React.useCallback((e: ClipboardEvent) => { | |
| const items = e.clipboardData?.items; | |
| if (!items) return; | |
| for (let i = 0; i < items.length; i++) { | |
| if (items[i].type.indexOf("image") !== -1) { | |
| const file = items[i].getAsFile(); | |
| if (file) { | |
| e.preventDefault(); | |
| processFile(file); | |
| break; | |
| } | |
| } | |
| } | |
| }, []); | |
| React.useEffect(() => { | |
| document.addEventListener("paste", handlePaste); | |
| return () => document.removeEventListener("paste", handlePaste); | |
| }, [handlePaste]); | |
| const handleSubmit = () => { | |
| if (input.trim() || files.length > 0) { | |
| let messagePrefix = ""; | |
| if (showSearch) messagePrefix = "[Search: "; | |
| else if (showThink) messagePrefix = "[Think: "; | |
| else if (showCanvas) messagePrefix = "[Canvas: "; | |
| const formattedInput = messagePrefix ? `${messagePrefix}${input}]` : input; | |
| onSend(formattedInput, files); | |
| setInput(""); | |
| setFiles([]); | |
| setFilePreviews({}); | |
| } | |
| }; | |
| const handleStartRecording = () => console.log("Started recording"); | |
| const handleStopRecording = (duration: number) => { | |
| console.log(`Stopped recording after ${duration} seconds`); | |
| setIsRecording(false); | |
| onSend(`[Voice message - ${duration} seconds]`, []); | |
| }; | |
| const hasContent = input.trim() !== "" || files.length > 0; | |
| return ( | |
| <> | |
| <PromptInput | |
| value={input} | |
| onValueChange={setInput} | |
| isLoading={isLoading} | |
| onSubmit={handleSubmit} | |
| className={cn( | |
| "w-full bg-[#1F2023] border-[#444444] shadow-[0_8px_30px_rgba(0,0,0,0.24)] transition-all duration-300 ease-in-out", | |
| isRecording && "border-red-500/70", | |
| className | |
| )} | |
| disabled={isLoading || isRecording} | |
| ref={ref || promptBoxRef} | |
| onDragOver={handleDragOver} | |
| onDragLeave={handleDragLeave} | |
| onDrop={handleDrop} | |
| > | |
| {files.length > 0 && !isRecording && ( | |
| <div className="flex flex-wrap gap-2 p-0 pb-1 tYou are given a task to integrate an existing React component in the codebase | |
| The codebase should support: | |
| - shadcn project structure | |
| - Tailwind CSS | |
| - Typescript | |
| If it doesn't, provide instructions on how to setup project via shadcn CLI, install Tailwind or Typescript. | |
| Determine the default path for components and styles. | |
| If default path for components is not /components/ui, provide instructions on why it's important to create this folder | |
| Copy-paste this component to /components/ui folder: | |
| ```tsx | |
| animated-ai-chat.tsx | |
| "use client"; | |
| import { useEffect, useRef, useCallback, useTransition } from "react"; | |
| import { useState } from "react"; | |
| import { cn } from "@/lib/utils"; | |
| import { | |
| ImageIcon, | |
| FileUp, | |
| Figma, | |
| MonitorIcon, | |
| CircleUserRound, | |
| ArrowUpIcon, | |
| Paperclip, | |
| PlusIcon, | |
| SendIcon, | |
| XIcon, | |
| LoaderIcon, | |
| Sparkles, | |
| Command, | |
| } from "lucide-react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import * as React from "react" | |
| interface UseAutoResizeTextareaProps { | |
| minHeight: number; | |
| maxHeight?: number; | |
| } | |
| function useAutoResizeTextarea({ | |
| minHeight, | |
| maxHeight, | |
| }: UseAutoResizeTextareaProps) { | |
| const textareaRef = useRef<HTMLTextAreaElement>(null); | |
| const adjustHeight = useCallback( | |
| (reset?: boolean) => { | |
| const textarea = textareaRef.current; | |
| if (!textarea) return; | |
| if (reset) { | |
| textarea.style.height = `${minHeight}px`; | |
| return; | |
| } | |
| textarea.style.height = `${minHeight}px`; | |
| const newHeight = Math.max( | |
| minHeight, | |
| Math.min( | |
| textarea.scrollHeight, | |
| maxHeight ?? Number.POSITIVE_INFINITY | |
| ) | |
| ); | |
| textarea.style.height = `${newHeight}px`; | |
| }, | |
| [minHeight, maxHeight] | |
| ); | |
| useEffect(() => { | |
| const textarea = textareaRef.current; | |
| if (textarea) { | |
| textarea.style.height = `${minHeight}px`; | |
| } | |
| }, [minHeight]); | |
| useEffect(() => { | |
| const handleResize = () => adjustHeight(); | |
| window.addEventListener("resize", handleResize); | |
| return () => window.removeEventListener("resize", handleResize); | |
| }, [adjustHeight]); | |
| return { textareaRef, adjustHeight }; | |
| } | |
| interface CommandSuggestion { | |
| icon: React.ReactNode; | |
| label: string; | |
| description: string; | |
| prefix: string; | |
| } | |
| interface TextareaProps | |
| extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { | |
| containerClassName?: string; | |
| showRing?: boolean; | |
| } | |
| const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( | |
| ({ className, containerClassName, showRing = true, ...props }, ref) => { | |
| const [isFocused, setIsFocused] = React.useState(false); | |
| return ( | |
| <div className={cn( | |
| "relative", | |
| containerClassName | |
| )}> | |
| <textarea | |
| className={cn( | |
| "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm", | |
| "transition-all duration-200 ease-in-out", | |
| "placeholder:text-muted-foreground", | |
| "disabled:cursor-not-allowed disabled:opacity-50", | |
| showRing ? "focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0" : "", | |
| className | |
| )} | |
| ref={ref} | |
| onFocus={() => setIsFocused(true)} | |
| onBlur={() => setIsFocused(false)} | |
| {...props} | |
| /> | |
| {showRing && isFocused && ( | |
| <motion.span | |
| className="absolute inset-0 rounded-md pointer-events-none ring-2 ring-offset-0 ring-violet-500/30" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| transition={{ duration: 0.2 }} | |
| /> | |
| )} | |
| {props.onChange && ( | |
| <div | |
| className="absolute bottom-2 right-2 opacity-0 w-2 h-2 bg-violet-500 rounded-full" | |
| style={{ | |
| animation: 'none', | |
| }} | |
| id="textarea-ripple" | |
| /> | |
| )} | |
| </div> | |
| ) | |
| } | |
| ) | |
| Textarea.displayName = "Textarea" | |
| export function AnimatedAIChat() { | |
| const [value, setValue] = useState(""); | |
| const [attachments, setAttachments] = useState<string[]>([]); | |
| const [isTyping, setIsTyping] = useState(false); | |
| const [isPending, startTransition] = useTransition(); | |
| const [activeSuggestion, setActiveSuggestion] = useState<number>(-1); | |
| const [showCommandPalette, setShowCommandPalette] = useState(false); | |
| const [recentCommand, setRecentCommand] = useState<string | null>(null); | |
| const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); | |
| const { textareaRef, adjustHeight } = useAutoResizeTextarea({ | |
| minHeight: 60, | |
| maxHeight: 200, | |
| }); | |
| const [inputFocused, setInputFocused] = useState(false); | |
| const commandPaletteRef = useRef<HTMLDivElement>(null); | |
| const commandSuggestions: CommandSuggestion[] = [ | |
| { | |
| icon: <ImageIcon className="w-4 h-4" />, | |
| label: "Clone UI", | |
| description: "Generate a UI from a screenshot", | |
| prefix: "/clone" | |
| }, | |
| { | |
| icon: <Figma className="w-4 h-4" />, | |
| label: "Import Figma", | |
| description: "Import a design from Figma", | |
| prefix: "/figma" | |
| }, | |
| { | |
| icon: <MonitorIcon className="w-4 h-4" />, | |
| label: "Create Page", | |
| description: "Generate a new web page", | |
| prefix: "/page" | |
| }, | |
| { | |
| icon: <Sparkles className="w-4 h-4" />, | |
| label: "Improve", | |
| description: "Improve existing UI design", | |
| prefix: "/improve" | |
| }, | |
| ]; | |
| useEffect(() => { | |
| if (value.startsWith('/') && !value.includes(' ')) { | |
| setShowCommandPalette(true); | |
| const matchingSuggestionIndex = commandSuggestions.findIndex( | |
| (cmd) => cmd.prefix.startsWith(value) | |
| ); | |
| if (matchingSuggestionIndex >= 0) { | |
| setActiveSuggestion(matchingSuggestionIndex); | |
| } else { | |
| setActiveSuggestion(-1); | |
| } | |
| } else { | |
| setShowCommandPalette(false); | |
| } | |
| }, [value]); | |
| useEffect(() => { | |
| const handleMouseMove = (e: MouseEvent) => { | |
| setMousePosition({ x: e.clientX, y: e.clientY }); | |
| }; | |
| window.addEventListener('mousemove', handleMouseMove); | |
| return () => { | |
| window.removeEventListener('mousemove', handleMouseMove); | |
| }; | |
| }, []); | |
| useEffect(() => { | |
| const handleClickOutside = (event: MouseEvent) => { | |
| const target = event.target as Node; | |
| const commandButton = document.querySelector('[data-command-button]'); | |
| if (commandPaletteRef.current && | |
| !commandPaletteRef.current.contains(target) && | |
| !commandButton?.contains(target)) { | |
| setShowCommandPalette(false); | |
| } | |
| }; | |
| document.addEventListener('mousedown', handleClickOutside); | |
| return () => { | |
| document.removeEventListener('mousedown', handleClickOutside); | |
| }; | |
| }, []); | |
| const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | |
| if (showCommandPalette) { | |
| if (e.key === 'ArrowDown') { | |
| e.preventDefault(); | |
| setActiveSuggestion(prev => | |
| prev < commandSuggestions.length - 1 ? prev + 1 : 0 | |
| ); | |
| } else if (e.key === 'ArrowUp') { | |
| e.preventDefault(); | |
| setActiveSuggestion(prev => | |
| prev > 0 ? prev - 1 : commandSuggestions.length - 1 | |
| ); | |
| } else if (e.key === 'Tab' || e.key === 'Enter') { | |
| e.preventDefault(); | |
| if (activeSuggestion >= 0) { | |
| const selectedCommand = commandSuggestions[activeSuggestion]; | |
| setValue(selectedCommand.prefix + ' '); | |
| setShowCommandPalette(false); | |
| setRecentCommand(selectedCommand.label); | |
| setTimeout(() => setRecentCommand(null), 3500); | |
| } | |
| } else if (e.key === 'Escape') { | |
| e.preventDefault(); | |
| setShowCommandPalette(false); | |
| } | |
| } else if (e.key === "Enter" && !e.shiftKey) { | |
| e.preventDefault(); | |
| if (value.trim()) { | |
| handleSendMessage(); | |
| } | |
| } | |
| }; | |
| const handleSendMessage = () => { | |
| if (value.trim()) { | |
| startTransition(() => { | |
| setIsTyping(true); | |
| setTimeout(() => { | |
| setIsTyping(false); | |
| setValue(""); | |
| adjustHeight(true); | |
| }, 3000); | |
| }); | |
| } | |
| }; | |
| const handleAttachFile = () => { | |
| const mockFileName = `file-${Math.floor(Math.random() * 1000)}.pdf`; | |
| setAttachments(prev => [...prev, mockFileName]); | |
| }; | |
| const removeAttachment = (index: number) => { | |
| setAttachments(prev => prev.filter((_, i) => i !== index)); | |
| }; | |
| const selectCommandSuggestion = (index: number) => { | |
| const selectedCommand = commandSuggestions[index]; | |
| setValue(selectedCommand.prefix + ' '); | |
| setShowCommandPalette(false); | |
| setRecentCommand(selectedCommand.label); | |
| setTimeout(() => setRecentCommand(null), 2000); | |
| }; | |
| return ( | |
| <div className="min-h-screen flex flex-col w-full items-center justify-center bg-transparent text-white p-6 relative overflow-hidden"> | |
| <div className="absolute inset-0 w-full h-full overflow-hidden"> | |
| <div className="absolute top-0 left-1/4 w-96 h-96 bg-violet-500/10 rounded-full mix-blend-normal filter blur-[128px] animate-pulse" /> | |
| <div className="absolute bottom-0 right-1/4 w-96 h-96 bg-indigo-500/10 rounded-full mix-blend-normal filter blur-[128px] animate-pulse delay-700" /> | |
| <div className="absolute top-1/4 right-1/3 w-64 h-64 bg-fuchsia-500/10 rounded-full mix-blend-normal filter blur-[96px] animate-pulse delay-1000" /> | |
| </div> | |
| <div className="w-full max-w-2xl mx-auto relative"> | |
| <motion.div | |
| className="relative z-10 space-y-12" | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.6, ease: "easeOut" }} | |
| > | |
| <div className="text-center space-y-3"> | |
| <motion.div | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.2, duration: 0.5 }} | |
| className="inline-block" | |
| > | |
| <h1 className="text-3xl font-medium tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white/90 to-white/40 pb-1"> | |
| How can I help today? | |
| </h1> | |
| <motion.div | |
| className="h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" | |
| initial={{ width: 0, opacity: 0 }} | |
| animate={{ width: "100%", opacity: 1 }} | |
| transition={{ delay: 0.5, duration: 0.8 }} | |
| /> | |
| </motion.div> | |
| <motion.p | |
| className="text-sm text-white/40" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ delay: 0.3 }} | |
| > | |
| Type a command or ask a question | |
| </motion.p> | |
| </div> | |
| <motion.div | |
| className="relative backdrop-blur-2xl bg-white/[0.02] rounded-2xl border border-white/[0.05] shadow-2xl" | |
| initial={{ scale: 0.98 }} | |
| animate={{ scale: 1 }} | |
| transition={{ delay: 0.1 }} | |
| > | |
| <AnimatePresence> | |
| {showCommandPalette && ( | |
| <motion.div | |
| ref={commandPaletteRef} | |
| className="absolute left-4 right-4 bottom-full mb-2 backdrop-blur-xl bg-black/90 rounded-lg z-50 shadow-lg border border-white/10 overflow-hidden" | |
| initial={{ opacity: 0, y: 5 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: 5 }} | |
| transition={{ duration: 0.15 }} | |
| > | |
| <div className="py-1 bg-black/95"> | |
| {commandSuggestions.map((suggestion, index) => ( | |
| <motion.div | |
| key={suggestion.prefix} | |
| className={cn( | |
| "flex items-center gap-2 px-3 py-2 text-xs transition-colors cursor-pointer", | |
| activeSuggestion === index | |
| ? "bg-white/10 text-white" | |
| : "text-white/70 hover:bg-white/5" | |
| )} | |
| onClick={() => selectCommandSuggestion(index)} | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ delay: index * 0.03 }} | |
| > | |
| <div className="w-5 h-5 flex items-center justify-center text-white/60"> | |
| {suggestion.icon} | |
| </div> | |
| <div className="font-medium">{suggestion.label}</div> | |
| <div className="text-white/40 text-xs ml-1"> | |
| {suggestion.prefix} | |
| </div> | |
| </motion.div> | |
| ))} | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| <div className="p-4"> | |
| <Textarea | |
| ref={textareaRef} | |
| value={value} | |
| onChange={(e) => { | |
| setValue(e.target.value); | |
| adjustHeight(); | |
| }} | |
| onKeyDown={handleKeyDown} | |
| onFocus={() => setInputFocused(true)} | |
| onBlur={() => setInputFocused(false)} | |
| placeholder="Ask zap a question..." | |
| containerClassName="w-full" | |
| className={cn( | |
| "w-full px-4 py-3", | |
| "resize-none", | |
| "bg-transparent", | |
| "border-none", | |
| "text-white/90 text-sm", | |
| "focus:outline-none", | |
| "placeholder:text-white/20", | |
| "min-h-[60px]" | |
| )} | |
| style={{ | |
| overflow: "hidden", | |
| }} | |
| showRing={false} | |
| /> | |
| </div> | |
| <AnimatePresence> | |
| {attachments.length > 0 && ( | |
| <motion.div | |
| className="px-4 pb-3 flex gap-2 flex-wrap" | |
| initial={{ opacity: 0, height: 0 }} | |
| animate={{ opacity: 1, height: "auto" }} | |
| exit={{ opacity: 0, height: 0 }} | |
| > | |
| {attachments.map((file, index) => ( | |
| <motion.div | |
| key={index} | |
| className="flex items-center gap-2 text-xs bg-white/[0.03] py-1.5 px-3 rounded-lg text-white/70" | |
| initial={{ opacity: 0, scale: 0.9 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| exit={{ opacity: 0, scale: 0.9 }} | |
| > | |
| <span>{file}</span> | |
| <button | |
| onClick={() => removeAttachment(index)} | |
| className="text-white/40 hover:text-white transition-colors" | |
| > | |
| <XIcon className="w-3 h-3" /> | |
| </button> | |
| </motion.div> | |
| ))} | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| <div className="p-4 border-t border-white/[0.05] flex items-center justify-between gap-4"> | |
| <div className="flex items-center gap-3"> | |
| <motion.button | |
| type="button" | |
| onClick={handleAttachFile} | |
| whileTap={{ scale: 0.94 }} | |
| className="p-2 text-white/40 hover:text-white/90 rounded-lg transition-colors relative group" | |
| > | |
| <Paperclip className="w-4 h-4" /> | |
| <motion.span | |
| className="absolute inset-0 bg-white/[0.05] rounded-lg opacity-0 group-hover:opacity-100 transition-opacity" | |
| layoutId="button-highlight" | |
| /> | |
| </motion.button> | |
| ransition-all duration-300"> | |
| {files.map((file, index) => ( | |
| <div key={index} className="relative group"> | |
| {file.type.startsWith("image/") && filePreviews[file.name] && ( | |
| <div | |
| className="w-16 h-16 rounded-xl overflow-hidden cursor-pointer transition-all duration-300" | |
| onClick={() => openImageModal(filePreviews[file.name])} | |
| > | |
| <img | |
| src={filePreviews[file.name]} | |
| alt={file.name} | |
| className | |
| That is two prompt |