Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
107 lines
3.1 KiB
TypeScript
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>
|
|
)
|
|
}
|