ultisuite-client/components/gmail/settings/automation/llm-providers-panel.tsx
R3D347HR4Y 9e9fd208ad
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(admin-settings): enhance admin settings with new components and layout improvements
- Introduced new components for managing admin settings, including AdminListControls, AdminSettingsCard, and TechBrandSelectLabel.
- Implemented dynamic loading for admin settings sections to optimize performance.
- Enhanced the layout of various admin settings sections for better user experience.
- Updated the AiAssistantSection to include LLM provider management and improved model selection.
- Refactored authentication settings to streamline configuration and improve accessibility.
2026-06-15 00:22:20 +02:00

142 lines
4.9 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
useLLMSettings,
useUpdateLLMSettings,
} from "@/lib/api/hooks/use-contact-discovery"
import type { ApiLLMSettings } from "@/lib/contacts/discovery-types"
import { LLMModelSuggestInput } from "@/components/gmail/settings/automation/llm-model-suggest-input"
import { AutomationTabMasonry } from "@/components/gmail/settings/automation/automation-tab-masonry"
import { LlmProvidersEditor } from "@/components/llm/llm-providers-editor"
import { TechBrandSelectLabel } from "@/components/admin/settings/tech-brand-select-label"
import { inferLlmProviderType, llmCatalogEntry, normalizeLlmProvider } from "@/lib/llm/llm-provider-catalog"
import {
CONTACTS_MUTED_TEXT,
CONTACTS_PRIMARY_BTN_CLASS,
} from "@/lib/contacts-chrome-classes"
import { cn } from "@/lib/utils"
export function LLMProvidersPanel() {
const { data: remote, isLoading } = useLLMSettings()
const updateSettings = useUpdateLLMSettings()
const [draft, setDraft] = useState<ApiLLMSettings>({
default_provider_id: "",
providers: [],
})
const [saved, setSaved] = useState(false)
useEffect(() => {
if (remote) {
setDraft({
...remote,
providers: (remote.providers ?? []).map(normalizeLlmProvider),
})
}
}, [remote])
async function handleSave() {
await updateSettings.mutateAsync(draft)
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
if (isLoading) {
return <p className={cn("text-sm", CONTACTS_MUTED_TEXT)}>Chargement</p>
}
return (
<div className="space-y-6">
<div>
<h3 className="text-base font-medium">Fournisseurs LLM</h3>
<p className={cn("mt-1 text-sm", CONTACTS_MUTED_TEXT)}>
API OpenAI-compatibles pour l&apos;enrichissement des contacts et le tri par règles.
</p>
</div>
<LlmProvidersEditor
providers={draft.providers}
defaultProviderId={draft.default_provider_id}
onProvidersChange={(providers) => setDraft((prev) => ({ ...prev, providers }))}
onDefaultProviderIdChange={(default_provider_id) =>
setDraft((prev) => ({ ...prev, default_provider_id }))
}
renderDefaultModelInput={({ provider, onChange }) => (
<LLMModelSuggestInput
baseUrl={provider.base_url}
apiKey={provider.api_key}
value={provider.default_model}
onChange={onChange}
placeholder="gpt-4o-mini"
/>
)}
/>
<AutomationTabMasonry columns={2}>
<div className="space-y-3 rounded-lg border border-border p-4">
<h4 className="text-sm font-medium">Découverte de contacts</h4>
<div className="grid gap-3 sm:grid-cols-2">
<div>
<Label className="text-xs">Fournisseur pour l&apos;enrichissement</Label>
<Select
value={draft.contact_discovery_provider_id ?? draft.default_provider_id}
onValueChange={(v) =>
setDraft((p) => ({ ...p, contact_discovery_provider_id: v }))
}
>
<SelectTrigger className="mt-1 h-9">
<SelectValue placeholder="Même que défaut" />
</SelectTrigger>
<SelectContent>
{draft.providers.map((p) => {
const type = inferLlmProviderType(p)
const entry = llmCatalogEntry(type)
return (
<SelectItem key={p.id} value={p.id}>
<TechBrandSelectLabel brand={type} icon={entry.icon}>
{p.name || entry.label}
</TechBrandSelectLabel>
</SelectItem>
)
})}
</SelectContent>
</Select>
</div>
<div className="sm:col-span-2">
<Label className="text-xs">Modèle LLM</Label>
<Input
className="mt-1 h-9"
value={draft.contact_discovery_model ?? ""}
onChange={(e) =>
setDraft((p) => ({ ...p, contact_discovery_model: e.target.value }))
}
placeholder="Laisser vide pour utiliser le modèle par défaut du fournisseur"
/>
</div>
</div>
</div>
</AutomationTabMasonry>
<div className="flex flex-wrap gap-2">
<Button
onClick={handleSave}
disabled={updateSettings.isPending}
className={CONTACTS_PRIMARY_BTN_CLASS}
>
{updateSettings.isPending ? "Enregistrement…" : saved ? "Enregistré ✓" : "Enregistrer"}
</Button>
</div>
</div>
)
}