ultisuite-client/components/gmail/settings/automation/llm-model-suggest-input.tsx
R3D347HR4Y 07d57f13a8
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Add Contact Avatar Features and Improve UI Components
- Introduced new ContactAvatar and ContactAvatarPicker components for enhanced avatar management in contact views.
- Updated ContactDetailView and ContactFormView to utilize the new avatar components, improving user experience when adding or editing contacts.
- Enhanced ContactHoverCard and ContactRow components to display avatars, providing a more visually appealing interface.
- Added loading and error states in ContactsListView for better user feedback during data fetching.
- Implemented a new ContactsLoadState component to handle loading and error scenarios in the contacts list.
- Updated package.json to include @formkit/auto-animate for improved UI animations.
2026-06-06 20:26:51 +02:00

107 lines
3.1 KiB
TypeScript

'use client'
import { useEffect, useMemo, useState } from 'react'
import { Input } from '@/components/ui/input'
import { useDiscoverLLMModels } from '@/lib/api/hooks/use-contact-discovery'
import { CONTACTS_MUTED_TEXT } from '@/lib/contacts-chrome-classes'
import { cn } from '@/lib/utils'
const MAX_SUGGESTIONS = 4
interface LLMModelSuggestInputProps {
baseUrl: string
apiKey?: string
value: string
onChange: (value: string) => void
placeholder?: string
className?: string
}
export function LLMModelSuggestInput({
baseUrl,
apiKey = '',
value,
onChange,
placeholder,
className,
}: LLMModelSuggestInputProps) {
const [open, setOpen] = useState(false)
const [debouncedBaseUrl, setDebouncedBaseUrl] = useState(baseUrl)
const [debouncedApiKey, setDebouncedApiKey] = useState(apiKey)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedBaseUrl(baseUrl)
setDebouncedApiKey(apiKey)
}, 400)
return () => clearTimeout(timer)
}, [baseUrl, apiKey])
const { data, isFetching, isError } = useDiscoverLLMModels(debouncedBaseUrl, debouncedApiKey)
const models = data?.models ?? []
const filtered = useMemo(() => {
const q = value.trim().toLowerCase()
const matches = q
? models.filter((model) => model.toLowerCase().includes(q))
: models
return matches.slice(0, MAX_SUGGESTIONS)
}, [models, value])
const showDropdown = open && !isFetching && filtered.length > 0
function pickModel(model: string) {
onChange(model)
setOpen(false)
}
return (
<div className="space-y-1">
<div className="relative">
<Input
className={cn('h-9', className)}
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
onFocus={() => setOpen(true)}
onBlur={() => {
window.setTimeout(() => setOpen(false), 150)
}}
onKeyDown={(e) => {
if (e.key === 'Escape') setOpen(false)
}}
/>
{showDropdown ? (
<ul className="absolute z-20 mt-1 w-full rounded-md border border-border bg-popover py-1 shadow-md">
{filtered.map((model) => (
<li key={model}>
<button
type="button"
className="w-full px-2 py-1.5 text-left text-xs hover:bg-muted"
onMouseDown={(e) => {
e.preventDefault()
pickModel(model)
}}
>
<span className="block truncate font-mono">{model}</span>
</button>
</li>
))}
</ul>
) : null}
</div>
{baseUrl.trim() ? (
<p className={cn('text-[11px]', CONTACTS_MUTED_TEXT)}>
{isFetching
? 'Chargement des modèles…'
: isError
? 'Impossible de récupérer les modèles pour cette URL.'
: models.length > 0
? `${models.length} modèle${models.length > 1 ? 's' : ''} disponible${models.length > 1 ? 's' : ''}.`
: 'Aucun modèle trouvé pour cette URL.'}
</p>
) : null}
</div>
)
}