"use client"; import { useAtomValue } from "jotai"; import { AlertCircle, Check, ChevronsUpDown } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms"; import { createVisionLLMConfigMutationAtom, updateVisionLLMConfigMutationAtom, } from "@/atoms/vision-llm-config/vision-llm-config-mutation.atoms"; import { visionModelListAtom } from "@/atoms/vision-llm-config/vision-llm-config-query.atoms"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Spinner } from "@/components/ui/spinner"; import { VISION_PROVIDERS } from "@/contracts/enums/vision-providers"; import type { GlobalVisionLLMConfig, VisionLLMConfig, VisionProvider, } from "@/contracts/types/new-llm-config.types"; import { cn } from "@/lib/utils"; interface VisionConfigDialogProps { open: boolean; onOpenChange: (open: boolean) => void; config: VisionLLMConfig | GlobalVisionLLMConfig | null; isGlobal: boolean; searchSpaceId: number; mode: "create" | "edit" | "view"; defaultProvider?: string; } const INITIAL_FORM = { name: "", description: "", provider: "", model_name: "", api_key: "", api_base: "", api_version: "", }; export function VisionConfigDialog({ open, onOpenChange, config, isGlobal, searchSpaceId, mode, defaultProvider, }: VisionConfigDialogProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState(INITIAL_FORM); const [scrollPos, setScrollPos] = useState<"top" | "middle" | "bottom">("top"); const scrollRef = useRef(null); useEffect(() => { if (open) { if (mode === "edit" && config && !isGlobal) { setFormData({ name: config.name || "", description: config.description || "", provider: config.provider || "", model_name: config.model_name || "", api_key: (config as VisionLLMConfig).api_key || "", api_base: config.api_base || "", api_version: (config as VisionLLMConfig).api_version || "", }); } else if (mode === "create") { setFormData({ ...INITIAL_FORM, provider: defaultProvider ?? "" }); } setScrollPos("top"); } }, [open, mode, config, isGlobal, defaultProvider]); const { mutateAsync: createConfig } = useAtomValue(createVisionLLMConfigMutationAtom); const { mutateAsync: updateConfig } = useAtomValue(updateVisionLLMConfigMutationAtom); const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom); const handleScroll = useCallback((e: React.UIEvent) => { const el = e.currentTarget; const atTop = el.scrollTop <= 2; const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2; setScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle"); }, []); const getTitle = () => { if (mode === "create") return "Add Vision Model"; if (isGlobal) return "View Global Vision Model"; return "Edit Vision Model"; }; const getSubtitle = () => { if (mode === "create") return "Set up a new vision-capable LLM provider"; if (isGlobal) return "Read-only global configuration"; return "Update your vision model settings"; }; const handleSubmit = useCallback(async () => { setIsSubmitting(true); try { if (mode === "create") { const result = await createConfig({ name: formData.name, provider: formData.provider as VisionProvider, model_name: formData.model_name, api_key: formData.api_key, api_base: formData.api_base || undefined, api_version: formData.api_version || undefined, description: formData.description || undefined, search_space_id: searchSpaceId, }); if (result?.id) { await updatePreferences({ search_space_id: searchSpaceId, data: { vision_llm_config_id: result.id }, }); } onOpenChange(false); } else if (!isGlobal && config) { await updateConfig({ id: config.id, data: { name: formData.name, description: formData.description || undefined, provider: formData.provider as VisionProvider, model_name: formData.model_name, api_key: formData.api_key, api_base: formData.api_base || undefined, api_version: formData.api_version || undefined, }, }); onOpenChange(false); } } catch (error) { console.error("Failed to save vision config:", error); toast.error("Failed to save vision model"); } finally { setIsSubmitting(false); } }, [ mode, isGlobal, config, formData, searchSpaceId, createConfig, updateConfig, updatePreferences, onOpenChange, ]); const handleUseGlobalConfig = useCallback(async () => { if (!config || !isGlobal) return; setIsSubmitting(true); try { await updatePreferences({ search_space_id: searchSpaceId, data: { vision_llm_config_id: config.id }, }); toast.success(`Now using ${config.name}`); onOpenChange(false); } catch (error) { console.error("Failed to set vision model:", error); toast.error("Failed to set vision model"); } finally { setIsSubmitting(false); } }, [config, isGlobal, searchSpaceId, updatePreferences, onOpenChange]); const { data: dynamicModels } = useAtomValue(visionModelListAtom); const [modelComboboxOpen, setModelComboboxOpen] = useState(false); const availableModels = useMemo( () => (dynamicModels ?? []).filter((m) => m.provider === formData.provider), [dynamicModels, formData.provider] ); const isFormValid = formData.name && formData.provider && formData.model_name && formData.api_key; const selectedProvider = VISION_PROVIDERS.find((p) => p.value === formData.provider); return ( e.preventDefault()} > {getTitle()}

{getTitle()}

{isGlobal && mode !== "create" && ( Global )}

{getSubtitle()}

{config && mode !== "create" && (

{config.model_name}

)}
{isGlobal && config && ( <> Global configurations are read-only. To customize, create a new model.
Name

{config.name}

{config.description && (
Description

{config.description}

)}
Provider

{config.provider}

Model

{config.model_name}

)} {(mode === "create" || (mode === "edit" && !isGlobal)) && (
setFormData((p) => ({ ...p, name: e.target.value }))} />
setFormData((p) => ({ ...p, description: e.target.value }))} />
setFormData((p) => ({ ...p, model_name: val }))} />
{formData.model_name ? `Using: "${formData.model_name}"` : "Type your model name"}
{availableModels.length > 0 && ( {availableModels .filter( (model) => !formData.model_name || model.value .toLowerCase() .includes(formData.model_name.toLowerCase()) || model.label .toLowerCase() .includes(formData.model_name.toLowerCase()) ) .slice(0, 50) .map((model) => ( { setFormData((p) => ({ ...p, model_name: value, })); setModelComboboxOpen(false); }} className="py-2" >
{model.label}
{model.contextWindow && (
Context: {model.contextWindow}
)}
))}
)}
setFormData((p) => ({ ...p, api_key: e.target.value }))} />
setFormData((p) => ({ ...p, api_base: e.target.value }))} />
{formData.provider === "AZURE_OPENAI" && (
setFormData((p) => ({ ...p, api_version: e.target.value }))} />
)}
)}
{mode === "create" || (mode === "edit" && !isGlobal) ? ( ) : isGlobal && config ? ( ) : null}
); }