feat(message-input-actions): update to use motion, improve animations

This commit is contained in:
ItzCrazyKns
2025-12-15 23:48:14 +05:30
parent f9cc97ffb5
commit 4bdb90e150
2 changed files with 156 additions and 148 deletions

View File

@@ -2,15 +2,11 @@
import { Cpu, Loader2, Search } from 'lucide-react'; import { Cpu, Loader2, Search } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react';
Popover, import { useEffect, useMemo, useState } from 'react';
PopoverButton,
PopoverPanel,
Transition,
} from '@headlessui/react';
import { Fragment, useEffect, useMemo, useState } from 'react';
import { MinimalProvider } from '@/lib/models/types'; import { MinimalProvider } from '@/lib/models/types';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
import { AnimatePresence, motion } from 'motion/react';
const ModelSelector = () => { const ModelSelector = () => {
const [providers, setProviders] = useState<MinimalProvider[]>([]); const [providers, setProviders] = useState<MinimalProvider[]>([]);
@@ -79,119 +75,127 @@ const ModelSelector = () => {
return ( return (
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg"> <Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
<PopoverButton {({ open }) => (
type="button" <>
className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" <PopoverButton
> type="button"
<Cpu size={16} className="text-sky-500" /> className="active:border-none hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg focus:outline-none headless-open:text-black dark:headless-open:text-white text-black/50 dark:text-white/50 active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
</PopoverButton> >
<Transition <Cpu size={16} className="text-sky-500" />
as={Fragment} </PopoverButton>
enter="transition ease-out duration-100" <AnimatePresence>
enterFrom="opacity-0 translate-y-1" {open && (
enterTo="opacity-100 translate-y-0" <PopoverPanel
leave="transition ease-in duration-100" className="absolute z-10 w-[230px] sm:w-[270px] md:w-[300px] right-0"
leaveFrom="opacity-100 translate-y-0" static
leaveTo="opacity-0 translate-y-1" >
> <motion.div
<PopoverPanel className="absolute z-10 w-[230px] sm:w-[270px] md:w-[300px] -right-4"> initial={{ opacity: 0, scale: 0.9 }}
<div className="bg-light-primary dark:bg-dark-primary max-h-[300px] sm:max-w-none border rounded-lg border-light-200 dark:border-dark-200 w-full flex flex-col shadow-lg overflow-hidden"> animate={{ opacity: 1, scale: 1 }}
<div className="p-4 border-b border-light-200 dark:border-dark-200"> exit={{ opacity: 0, scale: 0.9 }}
<div className="relative"> transition={{ duration: 0.1, ease: 'easeOut' }}
<Search className="origin-top-right bg-light-primary dark:bg-dark-primary max-h-[300px] sm:max-w-none border rounded-lg border-light-200 dark:border-dark-200 w-full flex flex-col shadow-lg overflow-hidden"
size={16} >
className="absolute left-3 top-1/2 -translate-y-1/2 text-black/40 dark:text-white/40" <div className="p-2 border-b border-light-200 dark:border-dark-200">
/> <div className="relative">
<input <Search
type="text" size={16}
placeholder="Search models..." className="absolute left-3 top-1/2 -translate-y-1/2 text-black/40 dark:text-white/40"
value={searchQuery} />
onChange={(e) => setSearchQuery(e.target.value)} <input
className="w-full pl-9 pr-3 py-2 bg-light-secondary dark:bg-dark-secondary rounded-lg placeholder:text-sm text-sm text-black dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-sky-500/20 border border-transparent focus:border-sky-500/30 transition duration-200" type="text"
/> placeholder="Search models..."
</div> value={searchQuery}
</div> onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-8 pr-3 py-2 bg-light-secondary dark:bg-dark-secondary rounded-lg placeholder:text-xs placeholder:-translate-y-[1.5px] text-xs text-black dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none border border-transparent transition duration-200"
/>
</div>
</div>
<div className="max-h-[320px] overflow-y-auto"> <div className="max-h-[320px] overflow-y-auto">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<Loader2 <Loader2
className="animate-spin text-black/40 dark:text-white/40" className="animate-spin text-black/40 dark:text-white/40"
size={24} size={24}
/> />
</div>
) : filteredProviders.length === 0 ? (
<div className="text-center py-16 px-4 text-black/60 dark:text-white/60 text-sm">
{searchQuery
? 'No models found'
: 'No chat models configured'}
</div>
) : (
<div className="flex flex-col">
{filteredProviders.map((provider, providerIndex) => (
<div key={provider.id}>
<div className="px-4 py-2.5 sticky top-0 bg-light-primary dark:bg-dark-primary border-b border-light-200/50 dark:border-dark-200/50">
<p className="text-xs text-black/50 dark:text-white/50 uppercase tracking-wider">
{provider.name}
</p>
</div> </div>
) : filteredProviders.length === 0 ? (
<div className="flex flex-col px-2 py-2 space-y-0.5"> <div className="text-center py-16 px-4 text-black/60 dark:text-white/60 text-sm">
{provider.chatModels.map((model) => ( {searchQuery
<button ? 'No models found'
key={model.key} : 'No chat models configured'}
onClick={() => </div>
handleModelSelect(provider.id, model.key) ) : (
} <div className="flex flex-col">
type="button" {filteredProviders.map((provider, providerIndex) => (
className={cn( <div key={provider.id}>
'px-3 py-2 flex items-center justify-between text-start duration-200 cursor-pointer transition rounded-lg group', <div className="px-4 py-2.5 sticky top-0 bg-light-primary dark:bg-dark-primary border-b border-light-200/50 dark:border-dark-200/50">
chatModelProvider?.providerId === provider.id && <p className="text-xs text-black/50 dark:text-white/50 uppercase tracking-wider">
chatModelProvider?.key === model.key {provider.name}
? 'bg-light-secondary dark:bg-dark-secondary'
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
)}
>
<div className="flex items-center space-x-2.5 min-w-0 flex-1">
<Cpu
size={15}
className={cn(
'shrink-0',
chatModelProvider?.providerId ===
provider.id &&
chatModelProvider?.key === model.key
? 'text-sky-500'
: 'text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70',
)}
/>
<p
className={cn(
'text-sm truncate',
chatModelProvider?.providerId ===
provider.id &&
chatModelProvider?.key === model.key
? 'text-sky-500 font-medium'
: 'text-black/70 dark:text-white/70 group-hover:text-black dark:group-hover:text-white',
)}
>
{model.name}
</p> </p>
</div> </div>
</button>
<div className="flex flex-col px-2 py-2 space-y-0.5">
{provider.chatModels.map((model) => (
<button
key={model.key}
onClick={() =>
handleModelSelect(provider.id, model.key)
}
type="button"
className={cn(
'px-3 py-2 flex items-center justify-between text-start duration-200 cursor-pointer transition rounded-lg group',
chatModelProvider?.providerId ===
provider.id &&
chatModelProvider?.key === model.key
? 'bg-light-secondary dark:bg-dark-secondary'
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
)}
>
<div className="flex items-center space-x-2.5 min-w-0 flex-1">
<Cpu
size={15}
className={cn(
'shrink-0',
chatModelProvider?.providerId ===
provider.id &&
chatModelProvider?.key === model.key
? 'text-sky-500'
: 'text-black/50 dark:text-white/50 group-hover:text-black/70 group-hover:dark:text-white/70',
)}
/>
<p
className={cn(
'text-xs truncate',
chatModelProvider?.providerId ===
provider.id &&
chatModelProvider?.key === model.key
? 'text-sky-500 font-medium'
: 'text-black/70 dark:text-white/70 group-hover:text-black dark:group-hover:text-white',
)}
>
{model.name}
</p>
</div>
</button>
))}
</div>
{providerIndex < filteredProviders.length - 1 && (
<div className="h-px bg-light-200 dark:bg-dark-200" />
)}
</div>
))} ))}
</div> </div>
)}
{providerIndex < filteredProviders.length - 1 && ( </div>
<div className="h-px bg-light-200 dark:bg-dark-200" /> </motion.div>
)} </PopoverPanel>
</div> )}
))} </AnimatePresence>
</div> </>
)} )}
</div>
</div>
</PopoverPanel>
</Transition>
</Popover> </Popover>
); );
}; };

View File

@@ -8,6 +8,7 @@ import {
} from '@headlessui/react'; } from '@headlessui/react';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { useChat } from '@/lib/hooks/useChat'; import { useChat } from '@/lib/hooks/useChat';
import { AnimatePresence, motion } from 'motion/react';
const OptimizationModes = [ const OptimizationModes = [
{ {
@@ -60,40 +61,43 @@ const Optimization = () => {
/> />
</div> </div>
</PopoverButton> </PopoverButton>
<Transition <AnimatePresence>
as={Fragment} {open && (
enter="transition ease-out duration-150" <PopoverPanel
enterFrom="opacity-0 translate-y-1" className="absolute z-10 w-64 md:w-[250px] left-0"
enterTo="opacity-100 translate-y-0" static
leave="transition ease-in duration-150" >
leaveFrom="opacity-100 translate-y-0" <motion.div
leaveTo="opacity-0 translate-y-1" initial={{ opacity: 0, scale: 0.9 }}
> animate={{ opacity: 1, scale: 1 }}
<PopoverPanel className="absolute z-10 w-64 md:w-[250px] left-0"> exit={{ opacity: 0, scale: 0.9 }}
<div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto"> transition={{ duration: 0.1, ease: 'easeOut' }}
{OptimizationModes.map((mode, i) => ( className="origin-top-left flex flex-col space-y-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-2 max-h-[200px] md:max-h-none overflow-y-auto"
<PopoverButton >
onClick={() => setOptimizationMode(mode.key)} {OptimizationModes.map((mode, i) => (
key={i} <PopoverButton
className={cn( onClick={() => setOptimizationMode(mode.key)}
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none', key={i}
optimizationMode === mode.key className={cn(
? 'bg-light-secondary dark:bg-dark-secondary' 'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition focus:outline-none',
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary', optimizationMode === mode.key
)} ? 'bg-light-secondary dark:bg-dark-secondary'
> : 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
<div className="flex flex-row items-center space-x-1 text-black dark:text-white"> )}
{mode.icon} >
<p className="text-sm font-medium">{mode.title}</p> <div className="flex flex-row items-center space-x-1 text-black dark:text-white">
</div> {mode.icon}
<p className="text-black/70 dark:text-white/70 text-xs"> <p className="text-xs font-medium">{mode.title}</p>
{mode.description} </div>
</p> <p className="text-black/70 dark:text-white/70 text-xs">
</PopoverButton> {mode.description}
))} </p>
</div> </PopoverButton>
</PopoverPanel> ))}
</Transition> </motion.div>
</PopoverPanel>
)}
</AnimatePresence>
</> </>
)} )}
</Popover> </Popover>