diff --git a/surfsense_web/atoms/image-gen-config/image-gen-config-mutation.atoms.ts b/surfsense_web/atoms/image-gen-config/image-gen-config-mutation.atoms.ts index dbaf441d0..dd8bcd324 100644 --- a/surfsense_web/atoms/image-gen-config/image-gen-config-mutation.atoms.ts +++ b/surfsense_web/atoms/image-gen-config/image-gen-config-mutation.atoms.ts @@ -24,13 +24,13 @@ export const createImageGenConfigMutationAtom = atomWithMutation((get) => { return imageGenConfigApiService.createConfig(request); }, onSuccess: () => { - toast.success("Image model configuration created"); + toast.success("Image model created"); queryClient.invalidateQueries({ queryKey: cacheKeys.imageGenConfigs.all(Number(searchSpaceId)), }); }, onError: (error: Error) => { - toast.error(error.message || "Failed to create image model configuration"); + toast.error(error.message || "Failed to create image model"); }, }; }); @@ -48,7 +48,7 @@ export const updateImageGenConfigMutationAtom = atomWithMutation((get) => { return imageGenConfigApiService.updateConfig(request); }, onSuccess: (_: UpdateImageGenConfigResponse, request: UpdateImageGenConfigRequest) => { - toast.success("Image model configuration updated"); + toast.success("Image model updated"); queryClient.invalidateQueries({ queryKey: cacheKeys.imageGenConfigs.all(Number(searchSpaceId)), }); @@ -57,7 +57,7 @@ export const updateImageGenConfigMutationAtom = atomWithMutation((get) => { }); }, onError: (error: Error) => { - toast.error(error.message || "Failed to update image model configuration"); + toast.error(error.message || "Failed to update image model"); }, }; }); @@ -75,7 +75,7 @@ export const deleteImageGenConfigMutationAtom = atomWithMutation((get) => { return imageGenConfigApiService.deleteConfig(id); }, onSuccess: (_, id: number) => { - toast.success("Image model configuration deleted"); + toast.success("Image model deleted"); queryClient.setQueryData( cacheKeys.imageGenConfigs.all(Number(searchSpaceId)), (oldData: GetImageGenConfigsResponse | undefined) => { @@ -85,7 +85,7 @@ export const deleteImageGenConfigMutationAtom = atomWithMutation((get) => { ); }, onError: (error: Error) => { - toast.error(error.message || "Failed to delete image model configuration"); + toast.error(error.message || "Failed to delete image model"); }, }; }); diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx index 45a07d5a1..06801372d 100644 --- a/surfsense_web/components/new-chat/chat-header.tsx +++ b/surfsense_web/components/new-chat/chat-header.tsx @@ -7,7 +7,7 @@ import type { ImageGenerationConfig, NewLLMConfigPublic, } from "@/contracts/types/new-llm-config.types"; -import { ImageConfigDialog } from "./image-config-dialog"; +import { ImageConfigDialog } from "@/components/shared/image-config-dialog"; import { ModelConfigDialog } from "./model-config-dialog"; import { ModelSelector } from "./model-selector"; diff --git a/surfsense_web/components/new-chat/image-config-dialog.tsx b/surfsense_web/components/new-chat/image-config-dialog.tsx deleted file mode 100644 index 12263bdb1..000000000 --- a/surfsense_web/components/new-chat/image-config-dialog.tsx +++ /dev/null @@ -1,558 +0,0 @@ -"use client"; - -import { useAtomValue } from "jotai"; -import { AlertCircle, Check, ChevronsUpDown, X } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -import { toast } from "sonner"; -import { - createImageGenConfigMutationAtom, - updateImageGenConfigMutationAtom, -} from "@/atoms/image-gen-config/image-gen-config-mutation.atoms"; -import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.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 { 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 { IMAGE_GEN_MODELS, IMAGE_GEN_PROVIDERS } from "@/contracts/enums/image-gen-providers"; -import type { - GlobalImageGenConfig, - ImageGenerationConfig, - ImageGenProvider, -} from "@/contracts/types/new-llm-config.types"; -import { cn } from "@/lib/utils"; - -interface ImageConfigDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - config: ImageGenerationConfig | GlobalImageGenConfig | null; - isGlobal: boolean; - searchSpaceId: number; - mode: "create" | "edit" | "view"; -} - -const INITIAL_FORM = { - name: "", - description: "", - provider: "", - model_name: "", - api_key: "", - api_base: "", - api_version: "", -}; - -export function ImageConfigDialog({ - open, - onOpenChange, - config, - isGlobal, - searchSpaceId, - mode, -}: ImageConfigDialogProps) { - const [isSubmitting, setIsSubmitting] = useState(false); - const [mounted, setMounted] = useState(false); - const [formData, setFormData] = useState(INITIAL_FORM); - const [modelComboboxOpen, setModelComboboxOpen] = useState(false); - const [scrollPos, setScrollPos] = useState<"top" | "middle" | "bottom">("top"); - const scrollRef = useRef(null); - - useEffect(() => { - setMounted(true); - }, []); - - 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 ImageGenerationConfig).api_key || "", - api_base: config.api_base || "", - api_version: config.api_version || "", - }); - } else if (mode === "create") { - setFormData(INITIAL_FORM); - } - setScrollPos("top"); - } - }, [open, mode, config, isGlobal]); - - const { mutateAsync: createConfig } = useAtomValue(createImageGenConfigMutationAtom); - const { mutateAsync: updateConfig } = useAtomValue(updateImageGenConfigMutationAtom); - const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom); - - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape" && open) onOpenChange(false); - }; - window.addEventListener("keydown", handleEscape); - return () => window.removeEventListener("keydown", handleEscape); - }, [open, onOpenChange]); - - 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 isAutoMode = config && "is_auto_mode" in config && config.is_auto_mode; - - const suggestedModels = useMemo(() => { - if (!formData.provider) return []; - return IMAGE_GEN_MODELS.filter((m) => m.provider === formData.provider); - }, [formData.provider]); - - const getTitle = () => { - if (mode === "create") return "Add Image Model"; - if (isAutoMode) return "Auto Mode (Fastest)"; - if (isGlobal) return "View Global Image Model"; - return "Edit Image Model"; - }; - - const getSubtitle = () => { - if (mode === "create") return "Set up a new image generation provider"; - if (isAutoMode) return "Automatically routes requests across providers"; - if (isGlobal) return "Read-only global configuration"; - return "Update your image model settings"; - }; - - const handleSubmit = useCallback(async () => { - setIsSubmitting(true); - try { - if (mode === "create") { - const result = await createConfig({ - name: formData.name, - provider: formData.provider as ImageGenProvider, - 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: { image_generation_config_id: result.id }, - }); - } - toast.success("Image model created and assigned!"); - onOpenChange(false); - } else if (!isGlobal && config) { - await updateConfig({ - id: config.id, - data: { - name: formData.name, - description: formData.description || undefined, - provider: formData.provider as ImageGenProvider, - model_name: formData.model_name, - api_key: formData.api_key, - api_base: formData.api_base || undefined, - api_version: formData.api_version || undefined, - }, - }); - toast.success("Image model updated!"); - onOpenChange(false); - } - } catch (error) { - console.error("Failed to save image config:", error); - toast.error("Failed to save image 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: { image_generation_config_id: config.id }, - }); - toast.success(`Now using ${config.name}`); - onOpenChange(false); - } catch (error) { - console.error("Failed to set image model:", error); - toast.error("Failed to set image model"); - } finally { - setIsSubmitting(false); - } - }, [config, isGlobal, searchSpaceId, updatePreferences, onOpenChange]); - - const isFormValid = formData.name && formData.provider && formData.model_name && formData.api_key; - const selectedProvider = IMAGE_GEN_PROVIDERS.find((p) => p.value === formData.provider); - - if (!mounted) return null; - - const dialogContent = ( - - {open && ( - <> - onOpenChange(false)} - /> - - -
e.stopPropagation()} - onKeyDown={(e) => { - if (e.key === "Escape") onOpenChange(false); - }} - > - {/* Header */} -
-
-
-

{getTitle()}

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

{getSubtitle()}

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

- {config.model_name} -

- )} -
- -
- - {/* Scrollable content */} -
- {isAutoMode && ( - - - Auto mode distributes image generation requests across all configured - providers for optimal performance and rate limit protection. - - - )} - - {isGlobal && !isAutoMode && 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 })) - } - /> -
- - - -
- - -
- -
- - {suggestedModels.length > 0 ? ( - - - - - - - - setFormData((p) => ({ ...p, model_name: val })) - } - /> - - - - Type a custom model name - - - - {suggestedModels.map((m) => ( - { - setFormData((p) => ({ ...p, model_name: m.value })); - setModelComboboxOpen(false); - }} - > - - {m.value} - - {m.label} - - - ))} - - - - - - ) : ( - - setFormData((p) => ({ ...p, model_name: e.target.value })) - } - /> - )} -
- -
- - 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 })) - } - /> -
- )} -
- )} -
- - {/* Fixed footer */} -
- - {mode === "create" || (mode === "edit" && !isGlobal) ? ( - - ) : isAutoMode ? ( - - ) : isGlobal && config ? ( - - ) : null} -
-
-
- - )} -
- ); - - return typeof document !== "undefined" ? createPortal(dialogContent, document.body) : null; -} diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx index 74de7c28c..7b0db5596 100644 --- a/surfsense_web/components/settings/image-model-manager.tsx +++ b/surfsense_web/components/settings/image-model-manager.tsx @@ -3,29 +3,22 @@ import { useAtomValue } from "jotai"; import { AlertCircle, - Check, - ChevronsUpDown, Edit3, Info, - Key, Plus, RefreshCw, Trash2, Wand2, } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; -import { toast } from "sonner"; +import { useMemo, useState } from "react"; import { - createImageGenConfigMutationAtom, deleteImageGenConfigMutationAtom, - updateImageGenConfigMutationAtom, } from "@/atoms/image-gen-config/image-gen-config-mutation.atoms"; import { globalImageGenConfigsAtom, imageGenConfigsAtom, } from "@/atoms/image-gen-config/image-gen-config-query.atoms"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; -import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertDialog, @@ -40,43 +33,14 @@ import { import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - 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 { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { - getImageGenModelsByProvider, - IMAGE_GEN_PROVIDERS, -} from "@/contracts/enums/image-gen-providers"; import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types"; import { useMediaQuery } from "@/hooks/use-media-query"; import { getProviderIcon } from "@/lib/provider-icons"; import { cn } from "@/lib/utils"; +import { ImageConfigDialog } from "@/components/shared/image-config-dialog"; interface ImageModelManagerProps { searchSpaceId: number; @@ -92,23 +56,12 @@ function getInitials(name: string): string { export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { const isDesktop = useMediaQuery("(min-width: 768px)"); - // Image gen config atoms - const { - mutateAsync: createConfig, - isPending: isCreating, - error: createError, - } = useAtomValue(createImageGenConfigMutationAtom); - const { - mutateAsync: updateConfig, - isPending: isUpdating, - error: updateError, - } = useAtomValue(updateImageGenConfigMutationAtom); + const { mutateAsync: deleteConfig, isPending: isDeleting, error: deleteError, } = useAtomValue(deleteImageGenConfigMutationAtom); - const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom); const { data: userConfigs, @@ -119,7 +72,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { const { data: globalConfigs = [], isFetching: globalLoading } = useAtomValue(globalImageGenConfigsAtom); - // Members for user resolution const { data: members } = useAtomValue(membersAtom); const memberMap = useMemo(() => { const map = new Map(); @@ -135,7 +87,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { return map; }, [members]); - // Permissions const { data: access } = useAtomValue(myAccessAtom); const canCreate = useMemo(() => { if (!access) return false; @@ -147,92 +98,25 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { if (access.is_owner) return true; return access.permissions?.includes("image_generations:delete") ?? false; }, [access]); - // Backend uses image_generations:create for update as well const canUpdate = canCreate; const isReadOnly = !canCreate && !canDelete; - // Local state const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingConfig, setEditingConfig] = useState(null); const [configToDelete, setConfigToDelete] = useState(null); - const isSubmitting = isCreating || isUpdating; const isLoading = configsLoading || globalLoading; - const errors = [createError, updateError, deleteError, fetchError].filter(Boolean) as Error[]; + const errors = [deleteError, fetchError].filter(Boolean) as Error[]; - // Form state for create/edit dialog - const [formData, setFormData] = useState({ - name: "", - description: "", - provider: "", - custom_provider: "", - model_name: "", - api_key: "", - api_base: "", - api_version: "", - }); - const [modelComboboxOpen, setModelComboboxOpen] = useState(false); - - const resetForm = () => { - setFormData({ - name: "", - description: "", - provider: "", - custom_provider: "", - model_name: "", - api_key: "", - api_base: "", - api_version: "", - }); + const openEditDialog = (config: ImageGenerationConfig) => { + setEditingConfig(config); + setIsDialogOpen(true); }; - const handleFormSubmit = useCallback(async () => { - if (!formData.name || !formData.provider || !formData.model_name || !formData.api_key) { - toast.error("Please fill in all required fields"); - return; - } - try { - if (editingConfig) { - await updateConfig({ - id: editingConfig.id, - data: { - name: formData.name, - description: formData.description || undefined, - provider: formData.provider as any, - custom_provider: formData.custom_provider || undefined, - model_name: formData.model_name, - api_key: formData.api_key, - api_base: formData.api_base || undefined, - api_version: formData.api_version || undefined, - }, - }); - } else { - const result = await createConfig({ - name: formData.name, - description: formData.description || undefined, - provider: formData.provider as any, - custom_provider: formData.custom_provider || undefined, - model_name: formData.model_name, - api_key: formData.api_key, - api_base: formData.api_base || undefined, - api_version: formData.api_version || undefined, - search_space_id: searchSpaceId, - }); - // Auto-assign newly created config - if (result?.id) { - await updatePreferences({ - search_space_id: searchSpaceId, - data: { image_generation_config_id: result.id }, - }); - } - } - setIsDialogOpen(false); - setEditingConfig(null); - resetForm(); - } catch { - // Error handled by mutation - } - }, [editingConfig, formData, searchSpaceId, createConfig, updateConfig, updatePreferences]); + const openNewDialog = () => { + setEditingConfig(null); + setIsDialogOpen(true); + }; const handleDelete = async () => { if (!configToDelete) return; @@ -244,30 +128,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { } }; - const openEditDialog = (config: ImageGenerationConfig) => { - setEditingConfig(config); - setFormData({ - name: config.name, - description: config.description || "", - provider: config.provider, - custom_provider: config.custom_provider || "", - model_name: config.model_name, - api_key: config.api_key, - api_base: config.api_base || "", - api_version: config.api_version || "", - }); - setIsDialogOpen(true); - }; - - const openNewDialog = () => { - setEditingConfig(null); - resetForm(); - setIsDialogOpen(true); - }; - - const selectedProvider = IMAGE_GEN_PROVIDERS.find((p) => p.value === formData.provider); - const suggestedModels = getImageGenModelsByProvider(formData.provider); - return (
{/* Header */} @@ -348,31 +208,26 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { {/* Loading Skeleton */} {isLoading && (
- {/* Your Image Models Section Skeleton */}
- {/* Cards Grid Skeleton */}
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => ( - {/* Header */}
- {/* Provider + Model */}
- {/* Footer */}
@@ -529,204 +384,18 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)} - {/* Create/Edit Dialog */} - { - if (!open) { - setIsDialogOpen(false); - setEditingConfig(null); - resetForm(); - } + setIsDialogOpen(open); + if (!open) setEditingConfig(null); }} - > - e.preventDefault()} - > - - {editingConfig ? "Edit Image Model" : "Add Image Model"} - - {editingConfig - ? "Update your image generation model" - : "Configure a new image generation model (DALL-E 3, GPT Image 1, etc.)"} - - - -
- {/* Name */} -
- - setFormData((p) => ({ ...p, name: e.target.value }))} - /> -
- - {/* Description */} -
- - setFormData((p) => ({ ...p, description: e.target.value }))} - /> -
- - - - {/* Provider */} -
- - -
- - {/* Model Name */} -
- - {suggestedModels.length > 0 ? ( - - - - - - - setFormData((p) => ({ ...p, model_name: val }))} - /> - - - - Type a custom model name - - - - {suggestedModels.map((m) => ( - { - setFormData((p) => ({ ...p, model_name: m.value })); - setModelComboboxOpen(false); - }} - > - - {m.value} - {m.label} - - ))} - - - - - - ) : ( - setFormData((p) => ({ ...p, model_name: e.target.value }))} - /> - )} -
- - {/* API Key */} -
- - setFormData((p) => ({ ...p, api_key: e.target.value }))} - /> -
- - {/* API Base (optional) */} -
- - setFormData((p) => ({ ...p, api_base: e.target.value }))} - /> -
- - {/* API Version (Azure) */} - {formData.provider === "AZURE_OPENAI" && ( -
- - setFormData((p) => ({ ...p, api_version: e.target.value }))} - /> -
- )} - - {/* Actions */} -
- - -
-
-
-
+ config={editingConfig} + isGlobal={false} + searchSpaceId={searchSpaceId} + mode={editingConfig ? "edit" : "create"} + /> {/* Delete Confirmation */} void; + config: ImageGenerationConfig | GlobalImageGenConfig | null; + isGlobal: boolean; + searchSpaceId: number; + mode: "create" | "edit" | "view"; +} + +const INITIAL_FORM = { + name: "", + description: "", + provider: "", + model_name: "", + api_key: "", + api_base: "", + api_version: "", +}; + +export function ImageConfigDialog({ + open, + onOpenChange, + config, + isGlobal, + searchSpaceId, + mode, +}: ImageConfigDialogProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState(INITIAL_FORM); + const [modelComboboxOpen, setModelComboboxOpen] = useState(false); + 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 ImageGenerationConfig).api_key || "", + api_base: config.api_base || "", + api_version: config.api_version || "", + }); + } else if (mode === "create") { + setFormData(INITIAL_FORM); + } + setScrollPos("top"); + } + }, [open, mode, config, isGlobal]); + + const { mutateAsync: createConfig } = useAtomValue(createImageGenConfigMutationAtom); + const { mutateAsync: updateConfig } = useAtomValue(updateImageGenConfigMutationAtom); + 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 isAutoMode = config && "is_auto_mode" in config && config.is_auto_mode; + + const suggestedModels = useMemo(() => { + if (!formData.provider) return []; + return IMAGE_GEN_MODELS.filter((m) => m.provider === formData.provider); + }, [formData.provider]); + + const getTitle = () => { + if (mode === "create") return "Add Image Model"; + if (isAutoMode) return "Auto Mode (Fastest)"; + if (isGlobal) return "View Global Image Model"; + return "Edit Image Model"; + }; + + const getSubtitle = () => { + if (mode === "create") return "Set up a new image generation provider"; + if (isAutoMode) return "Automatically routes requests across providers"; + if (isGlobal) return "Read-only global configuration"; + return "Update your image model settings"; + }; + + const handleSubmit = useCallback(async () => { + setIsSubmitting(true); + try { + if (mode === "create") { + const result = await createConfig({ + name: formData.name, + provider: formData.provider as ImageGenProvider, + 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: { image_generation_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 ImageGenProvider, + 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 image config:", error); + toast.error("Failed to save image 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: { image_generation_config_id: config.id }, + }); + toast.success(`Now using ${config.name}`); + onOpenChange(false); + } catch (error) { + console.error("Failed to set image model:", error); + toast.error("Failed to set image model"); + } finally { + setIsSubmitting(false); + } + }, [config, isGlobal, searchSpaceId, updatePreferences, onOpenChange]); + + const isFormValid = formData.name && formData.provider && formData.model_name && formData.api_key; + const selectedProvider = IMAGE_GEN_PROVIDERS.find((p) => p.value === formData.provider); + + return ( + + e.preventDefault()} + > + {getTitle()} + + {/* Header */} +
+
+
+

{getTitle()}

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

{getSubtitle()}

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

+ {config.model_name} +

+ )} +
+
+ + {/* Scrollable content */} +
+ {isAutoMode && ( + + + Auto mode distributes image generation requests across all configured + providers for optimal performance and rate limit protection. + + + )} + + {isGlobal && !isAutoMode && 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 })) + } + /> +
+ + + +
+ + +
+ +
+ + {suggestedModels.length > 0 ? ( + + + + + + + + setFormData((p) => ({ ...p, model_name: val })) + } + /> + + + + Type a custom model name + + + + {suggestedModels.map((m) => ( + { + setFormData((p) => ({ ...p, model_name: m.value })); + setModelComboboxOpen(false); + }} + > + + {m.value} + + {m.label} + + + ))} + + + + + + ) : ( + + setFormData((p) => ({ ...p, model_name: e.target.value })) + } + /> + )} +
+ +
+ + 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 })) + } + /> +
+ )} +
+ )} +
+ + {/* Fixed footer */} +
+ + {mode === "create" || (mode === "edit" && !isGlobal) ? ( + + ) : isAutoMode ? ( + + ) : isGlobal && config ? ( + + ) : null} +
+
+
+ ); +}