mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-12-16 16:48:14 +00:00
feat(message-input-actions): update to use motion, improve animations
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user