diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx index 0170fcf8c..37a1636c7 100644 --- a/surfsense_web/components/settings/image-model-manager.tsx +++ b/surfsense_web/components/settings/image-model-manager.tsx @@ -6,16 +6,18 @@ import { Check, ChevronsUpDown, Edit3, + ImageIcon, Key, Plus, RefreshCw, + Shuffle, Info, Trash2, Wand2, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import Image from "next/image"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; import { @@ -42,7 +44,7 @@ import { } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Command, CommandEmpty, @@ -129,7 +131,7 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { } = useAtomValue(imageGenConfigsAtom); const { data: globalConfigs = [], isFetching: globalLoading } = useAtomValue(globalImageGenConfigsAtom); - const { isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom); + const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom); // Members for user resolution const { data: members } = useAtomValue(membersAtom); @@ -168,6 +170,18 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { const [editingConfig, setEditingConfig] = useState(null); const [configToDelete, setConfigToDelete] = useState(null); + // Preference state + const [selectedPrefId, setSelectedPrefId] = useState( + preferences.image_generation_config_id ?? "" + ); + const [hasPrefChanges, setHasPrefChanges] = useState(false); + const [isSavingPref, setIsSavingPref] = useState(false); + + useEffect(() => { + setSelectedPrefId(preferences.image_generation_config_id ?? ""); + setHasPrefChanges(false); + }, [preferences]); + const isSubmitting = isCreating || isUpdating; const isLoading = configsLoading || globalLoading || prefsLoading; const errors = [createError, updateError, deleteError, fetchError].filter(Boolean) as Error[]; @@ -277,6 +291,39 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { setIsDialogOpen(true); }; + const handlePrefChange = (value: string) => { + const newVal = value === "unassigned" ? "" : parseInt(value); + setSelectedPrefId(newVal); + setHasPrefChanges(newVal !== (preferences.image_generation_config_id ?? "")); + }; + + const handleSavePref = async () => { + setIsSavingPref(true); + try { + await updatePreferences({ + search_space_id: searchSpaceId, + data: { + image_generation_config_id: + typeof selectedPrefId === "string" + ? selectedPrefId + ? parseInt(selectedPrefId) + : undefined + : selectedPrefId, + }, + }); + setHasPrefChanges(false); + toast.success("Image generation model preference saved!"); + } catch { + toast.error("Failed to save preference"); + } finally { + setIsSavingPref(false); + } + }; + + const allConfigs = [ + ...globalConfigs.map((c) => ({ ...c, _source: "global" as const })), + ...(userConfigs ?? []).map((c) => ({ ...c, _source: "user" as const })), + ]; const selectedProvider = IMAGE_GEN_PROVIDERS.find((p) => p.value === formData.provider); const suggestedModels = getImageGenModelsByProvider(formData.provider); @@ -356,9 +403,135 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { )} + {/* Active Preference Card */} + {!isLoading && allConfigs.length > 0 && ( + + + +
+
+ +
+
+ Active Image Model + + Select which model to use for image generation + +
+
+
+ + + {hasPrefChanges && ( +
+ + +
+ )} +
+
+
+ )} + {/* Loading Skeleton */} {isLoading && (
+ {/* Active Preference Skeleton */} + + +
+ +
+ + +
+
+
+ + + +
+ {/* Your Image Models Section Skeleton */}