diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx index 37a1636c7..f2d010e10 100644 --- a/surfsense_web/components/settings/image-model-manager.tsx +++ b/surfsense_web/components/settings/image-model-manager.tsx @@ -6,18 +6,16 @@ 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, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; import { @@ -30,7 +28,6 @@ import { imageGenConfigsAtom, } from "@/atoms/image-gen-config/image-gen-config-query.atoms"; import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms"; -import { llmPreferencesAtom } from "@/atoms/new-llm-config/new-llm-config-query.atoms"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertDialog, @@ -42,9 +39,8 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Command, CommandEmpty, @@ -131,7 +127,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { } = useAtomValue(imageGenConfigsAtom); const { data: globalConfigs = [], isFetching: globalLoading } = useAtomValue(globalImageGenConfigsAtom); - const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom); // Members for user resolution const { data: members } = useAtomValue(membersAtom); @@ -170,20 +165,8 @@ 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 isLoading = configsLoading || globalLoading; const errors = [createError, updateError, deleteError, fetchError].filter(Boolean) as Error[]; // Form state for create/edit dialog @@ -291,40 +274,6 @@ 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); @@ -342,6 +291,14 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { Refresh + {canCreate && ( + + )} {/* Errors */} @@ -403,135 +360,9 @@ 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 */}
@@ -573,18 +404,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { {/* User Configs */} {!isLoading && (
-
-

Your Image Models

- {canCreate && ( - - )} -
- {(userConfigs?.length ?? 0) === 0 ? ( diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx index c634ffd49..6e44b8958 100644 --- a/surfsense_web/components/settings/llm-role-manager.tsx +++ b/surfsense_web/components/settings/llm-role-manager.tsx @@ -7,6 +7,7 @@ import { CheckCircle, CircleDashed, FileText, + ImageIcon, RefreshCw, RotateCcw, Save, @@ -15,6 +16,10 @@ import { import { AnimatePresence, motion } from "motion/react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { + globalImageGenConfigsAtom, + imageGenConfigsAtom, +} from "@/atoms/image-gen-config/image-gen-config-query.atoms"; import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms"; import { globalNewLLMConfigsAtom, @@ -46,6 +51,8 @@ const ROLE_DESCRIPTIONS = { description: "Primary LLM for chat interactions and agent operations", color: "text-blue-600 dark:text-blue-400", bgColor: "bg-blue-500/10", + prefKey: "agent_llm_id" as const, + configType: "llm" as const, }, document_summary: { icon: FileText, @@ -53,6 +60,17 @@ const ROLE_DESCRIPTIONS = { description: "Handles document summarization and research synthesis", color: "text-purple-600 dark:text-purple-400", bgColor: "bg-purple-500/10", + prefKey: "document_summary_llm_id" as const, + configType: "llm" as const, + }, + image_generation: { + icon: ImageIcon, + title: "Image Generation Model", + description: "Model used for AI image generation (DALL-E, GPT Image, etc.)", + color: "text-teal-600 dark:text-teal-400", + bgColor: "bg-teal-500/10", + prefKey: "image_generation_config_id" as const, + configType: "image" as const, }, }; @@ -61,6 +79,7 @@ interface LLMRoleManagerProps { } export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { + // LLM configs const { data: newLLMConfigs = [], isFetching: configsLoading, @@ -72,6 +91,20 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { isFetching: globalConfigsLoading, error: globalConfigsError, } = useAtomValue(globalNewLLMConfigsAtom); + + // Image gen configs + const { + data: userImageConfigs = [], + isFetching: imageConfigsLoading, + error: imageConfigsError, + } = useAtomValue(imageGenConfigsAtom); + const { + data: globalImageConfigs = [], + isFetching: globalImageConfigsLoading, + error: globalImageConfigsError, + } = useAtomValue(globalImageGenConfigsAtom); + + // Preferences const { data: preferences = {}, isFetching: preferencesLoading, @@ -83,6 +116,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { const [assignments, setAssignments] = useState({ agent_llm_id: preferences.agent_llm_id ?? "", document_summary_llm_id: preferences.document_summary_llm_id ?? "", + image_generation_config_id: preferences.image_generation_config_id ?? "", }); const [hasChanges, setHasChanges] = useState(false); @@ -92,15 +126,16 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { const newAssignments = { agent_llm_id: preferences.agent_llm_id ?? "", document_summary_llm_id: preferences.document_summary_llm_id ?? "", + image_generation_config_id: preferences.image_generation_config_id ?? "", }; setAssignments(newAssignments); setHasChanges(false); }, [preferences]); - const handleRoleAssignment = (role: string, configId: string) => { + const handleRoleAssignment = (prefKey: string, configId: string) => { const newAssignments = { ...assignments, - [role]: configId === "unassigned" ? "" : parseInt(configId), + [prefKey]: configId === "unassigned" ? "" : parseInt(configId), }; setAssignments(newAssignments); @@ -108,6 +143,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { const currentPrefs = { agent_llm_id: preferences.agent_llm_id ?? "", document_summary_llm_id: preferences.document_summary_llm_id ?? "", + image_generation_config_id: preferences.image_generation_config_id ?? "", }; const hasChangesNow = Object.keys(newAssignments).some( @@ -122,19 +158,13 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { const handleSave = async () => { setIsSaving(true); + const toNumericOrUndefined = (val: string | number) => + typeof val === "string" ? (val ? parseInt(val) : undefined) : val; + const numericAssignments = { - agent_llm_id: - typeof assignments.agent_llm_id === "string" - ? assignments.agent_llm_id - ? parseInt(assignments.agent_llm_id) - : undefined - : assignments.agent_llm_id, - document_summary_llm_id: - typeof assignments.document_summary_llm_id === "string" - ? assignments.document_summary_llm_id - ? parseInt(assignments.document_summary_llm_id) - : undefined - : assignments.document_summary_llm_id, + agent_llm_id: toNumericOrUndefined(assignments.agent_llm_id), + document_summary_llm_id: toNumericOrUndefined(assignments.document_summary_llm_id), + image_generation_config_id: toNumericOrUndefined(assignments.image_generation_config_id), }; await updatePreferences({ @@ -143,7 +173,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { }); setHasChanges(false); - toast.success("LLM role assignments saved successfully!"); + toast.success("Role assignments saved successfully!"); setIsSaving(false); }; @@ -152,6 +182,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { setAssignments({ agent_llm_id: preferences.agent_llm_id ?? "", document_summary_llm_id: preferences.document_summary_llm_id ?? "", + image_generation_config_id: preferences.image_generation_config_id ?? "", }); setHasChanges(false); }; @@ -162,16 +193,26 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { assignments.agent_llm_id !== undefined && assignments.document_summary_llm_id !== "" && assignments.document_summary_llm_id !== null && - assignments.document_summary_llm_id !== undefined; + assignments.document_summary_llm_id !== undefined && + assignments.image_generation_config_id !== "" && + assignments.image_generation_config_id !== null && + assignments.image_generation_config_id !== undefined; - // Combine global and custom configs - const allConfigs = [ + // Combine global and custom LLM configs + const allLLMConfigs = [ ...globalConfigs.map((config) => ({ ...config, is_global: true })), ...newLLMConfigs.filter((config) => config.id && config.id.toString().trim() !== ""), ]; - const isLoading = configsLoading || preferencesLoading || globalConfigsLoading; - const hasError = configsError || preferencesError || globalConfigsError; + // Combine global and custom image gen configs + const allImageConfigs = [ + ...globalImageConfigs.map((config) => ({ ...config, is_global: true })), + ...(userImageConfigs ?? []).filter((config) => config.id && config.id.toString().trim() !== ""), + ]; + + const isLoading = configsLoading || preferencesLoading || globalConfigsLoading || imageConfigsLoading || globalImageConfigsLoading; + const hasError = configsError || preferencesError || globalConfigsError || imageConfigsError || globalImageConfigsError; + const hasAnyConfigs = allLLMConfigs.length > 0 || allImageConfigs.length > 0; return (
@@ -223,7 +264,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { {/* Loading Skeleton */} {isLoading && (
- {["skeleton-a", "skeleton-b"].map((key) => ( + {["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => ( {/* Header: icon + title + status */} @@ -260,18 +301,18 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { )} {/* No configs warning */} - {!isLoading && !hasError && allConfigs.length === 0 && ( + {!isLoading && !hasError && !hasAnyConfigs && ( - No LLM configurations found. Please add at least one LLM provider in the - Agent Configs tab before assigning roles. + No configurations found. Please add at least one LLM provider or image model + in the respective settings tabs before assigning roles. )} {/* Role Assignment Cards */} - {!isLoading && !hasError && allConfigs.length > 0 && ( + {!isLoading && !hasError && hasAnyConfigs && ( {Object.entries(ROLE_DESCRIPTIONS).map(([key, role], index) => { const IconComponent = role.icon; + const isImageRole = role.configType === "image"; const currentAssignment = - assignments[`${key}_llm_id` as keyof typeof assignments]; - const assignedConfig = allConfigs.find( + assignments[role.prefKey as keyof typeof assignments]; + + // Pick the right config lists based on role type + const roleGlobalConfigs = isImageRole ? globalImageConfigs : globalConfigs; + const roleUserConfigs = isImageRole + ? (userImageConfigs ?? []).filter((c) => c.id && c.id.toString().trim() !== "") + : newLLMConfigs.filter((c) => c.id && c.id.toString().trim() !== ""); + const roleAllConfigs = isImageRole ? allImageConfigs : allLLMConfigs; + + const assignedConfig = roleAllConfigs.find( (config) => config.id === currentAssignment ); const isAssigned = @@ -340,7 +390,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {