diff --git a/surfsense_web/atoms/new-llm-config/new-llm-config-mutation.atoms.ts b/surfsense_web/atoms/new-llm-config/new-llm-config-mutation.atoms.ts index 8f81b7475..b3b9b2bab 100644 --- a/surfsense_web/atoms/new-llm-config/new-llm-config-mutation.atoms.ts +++ b/surfsense_web/atoms/new-llm-config/new-llm-config-mutation.atoms.ts @@ -26,7 +26,7 @@ export const createNewLLMConfigMutationAtom = atomWithMutation((get) => { return newLLMConfigApiService.createConfig(request); }, onSuccess: () => { - toast.success("Configuration created successfully"); + toast.success("LLM model created"); queryClient.invalidateQueries({ queryKey: cacheKeys.newLLMConfigs.all(Number(searchSpaceId)), }); @@ -50,7 +50,7 @@ export const updateNewLLMConfigMutationAtom = atomWithMutation((get) => { return newLLMConfigApiService.updateConfig(request); }, onSuccess: (_: UpdateNewLLMConfigResponse, request: UpdateNewLLMConfigRequest) => { - toast.success("Configuration updated successfully"); + toast.success("LLM model updated"); queryClient.invalidateQueries({ queryKey: cacheKeys.newLLMConfigs.all(Number(searchSpaceId)), }); @@ -77,7 +77,7 @@ export const deleteNewLLMConfigMutationAtom = atomWithMutation((get) => { return newLLMConfigApiService.deleteConfig(request); }, onSuccess: (_, request: DeleteNewLLMConfigRequest) => { - toast.success("Configuration deleted successfully"); + toast.success("LLM model deleted"); queryClient.setQueryData( cacheKeys.newLLMConfigs.all(Number(searchSpaceId)), (oldData: GetNewLLMConfigsResponse | undefined) => { diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx index 06801372d..5b7d3500c 100644 --- a/surfsense_web/components/new-chat/chat-header.tsx +++ b/surfsense_web/components/new-chat/chat-header.tsx @@ -8,7 +8,7 @@ import type { NewLLMConfigPublic, } from "@/contracts/types/new-llm-config.types"; import { ImageConfigDialog } from "@/components/shared/image-config-dialog"; -import { ModelConfigDialog } from "./model-config-dialog"; +import { ModelConfigDialog } from "@/components/shared/model-config-dialog"; import { ModelSelector } from "./model-selector"; interface ChatHeaderProps { diff --git a/surfsense_web/components/new-chat/model-config-dialog.tsx b/surfsense_web/components/new-chat/model-config-dialog.tsx deleted file mode 100644 index 06ec3b9b5..000000000 --- a/surfsense_web/components/new-chat/model-config-dialog.tsx +++ /dev/null @@ -1,489 +0,0 @@ -"use client"; - -import { useAtomValue } from "jotai"; -import { AlertCircle, X, Zap } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -import { toast } from "sonner"; -import { - createNewLLMConfigMutationAtom, - updateLLMPreferencesMutationAtom, - updateNewLLMConfigMutationAtom, -} from "@/atoms/new-llm-config/new-llm-config-mutation.atoms"; -import { LLMConfigForm, type LLMConfigFormData } from "@/components/shared/llm-config-form"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Spinner } from "@/components/ui/spinner"; -import type { - GlobalNewLLMConfig, - LiteLLMProvider, - NewLLMConfigPublic, -} from "@/contracts/types/new-llm-config.types"; -import { cn } from "@/lib/utils"; - -interface ModelConfigDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - config: NewLLMConfigPublic | GlobalNewLLMConfig | null; - isGlobal: boolean; - searchSpaceId: number; - mode: "create" | "edit" | "view"; -} - -export function ModelConfigDialog({ - open, - onOpenChange, - config, - isGlobal, - searchSpaceId, - mode, -}: ModelConfigDialogProps) { - const [isSubmitting, setIsSubmitting] = useState(false); - const [mounted, setMounted] = useState(false); - const [scrollPos, setScrollPos] = useState<"top" | "middle" | "bottom">("top"); - const scrollRef = useRef(null); - - useEffect(() => { - setMounted(true); - }, []); - - 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 { mutateAsync: createConfig } = useAtomValue(createNewLLMConfigMutationAtom); - const { mutateAsync: updateConfig } = useAtomValue(updateNewLLMConfigMutationAtom); - 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 isAutoMode = config && "is_auto_mode" in config && config.is_auto_mode; - - const getTitle = () => { - if (mode === "create") return "Add New Configuration"; - if (isAutoMode) return "Auto Mode (Fastest)"; - if (isGlobal) return "View Global Configuration"; - return "Edit Configuration"; - }; - - const getSubtitle = () => { - if (mode === "create") return "Set up a new LLM provider for this search space"; - if (isAutoMode) return "Automatically routes requests across providers"; - if (isGlobal) return "Read-only global configuration"; - return "Update your configuration settings"; - }; - - const handleSubmit = useCallback( - async (data: LLMConfigFormData) => { - setIsSubmitting(true); - try { - if (mode === "create") { - const result = await createConfig({ - ...data, - search_space_id: searchSpaceId, - }); - - if (result?.id) { - await updatePreferences({ - search_space_id: searchSpaceId, - data: { - agent_llm_id: result.id, - }, - }); - } - - toast.success("Configuration created and assigned!"); - onOpenChange(false); - } else if (!isGlobal && config) { - await updateConfig({ - id: config.id, - data: { - name: data.name, - description: data.description, - provider: data.provider, - custom_provider: data.custom_provider, - model_name: data.model_name, - api_key: data.api_key, - api_base: data.api_base, - litellm_params: data.litellm_params, - system_instructions: data.system_instructions, - use_default_system_instructions: data.use_default_system_instructions, - citations_enabled: data.citations_enabled, - }, - }); - toast.success("Configuration updated!"); - onOpenChange(false); - } - } catch (error) { - console.error("Failed to save configuration:", error); - toast.error("Failed to save configuration"); - } finally { - setIsSubmitting(false); - } - }, - [ - mode, - isGlobal, - config, - searchSpaceId, - createConfig, - updateConfig, - updatePreferences, - onOpenChange, - ] - ); - - const handleUseGlobalConfig = useCallback(async () => { - if (!config || !isGlobal) return; - setIsSubmitting(true); - try { - await updatePreferences({ - search_space_id: searchSpaceId, - data: { - agent_llm_id: config.id, - }, - }); - toast.success(`Now using ${config.name}`); - onOpenChange(false); - } catch (error) { - console.error("Failed to set model:", error); - toast.error("Failed to set model"); - } finally { - setIsSubmitting(false); - } - }, [config, isGlobal, searchSpaceId, updatePreferences, onOpenChange]); - - if (!mounted) return null; - - const dialogContent = ( - - {open && ( - <> - {/* Backdrop */} - onOpenChange(false)} - /> - - {/* Dialog */} - -
e.stopPropagation()} - onKeyDown={(e) => { - if (e.key === "Escape") onOpenChange(false); - }} - > - {/* Header */} -
-
-
-

{getTitle()}

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

{getSubtitle()}

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

- {config.model_name} -

- )} -
- -
- - {/* Scrollable content */} -
- {isAutoMode && ( - - - Auto mode automatically distributes requests across all available LLM - providers to optimize performance and avoid rate limits. - - - )} - - {isGlobal && !isAutoMode && mode !== "create" && ( - - - - Global configurations are read-only. To customize settings, create a new - configuration based on this template. - - - )} - - {mode === "create" ? ( - - ) : isAutoMode && config ? ( -
-
-
-
- How It Works -
-

{config.description}

-
- -
- -
-
- Key Benefits -
-
-
- -
-

- Automatic (Fastest) -

-

- Distributes requests across all configured LLM providers -

-
-
-
- -
-

- Rate Limit Protection -

-

- Automatically handles rate limits with cooldowns and retries -

-
-
-
- -
-

- Automatic Failover -

-

- Falls back to other providers if one becomes unavailable -

-
-
-
-
-
-
- ) : isGlobal && config ? ( -
-
-
-
-
- Configuration Name -
-

{config.name}

-
- {config.description && ( -
-
- Description -
-

{config.description}

-
- )} -
- -
- -
-
-
- Provider -
-

{config.provider}

-
-
-
- Model -
-

{config.model_name}

-
-
- -
- -
-
-
- Citations -
- - {config.citations_enabled ? "Enabled" : "Disabled"} - -
-
- - {config.system_instructions && ( - <> -
-
-
- System Instructions -
-
-

- {config.system_instructions} -

-
-
- - )} -
-
- ) : config ? ( - - ) : null} -
- - {/* Fixed footer */} -
- - {mode === "create" || (!isGlobal && !isAutoMode && config) ? ( - - ) : isAutoMode ? ( - - ) : isGlobal && config ? ( - - ) : null} -
-
- - - )} - - ); - - return typeof document !== "undefined" ? createPortal(dialogContent, document.body) : null; -} diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx index 64bd7455f..a20086492 100644 --- a/surfsense_web/components/settings/model-config-manager.tsx +++ b/surfsense_web/components/settings/model-config-manager.tsx @@ -12,18 +12,16 @@ import { Trash2, Wand2, } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; import { - createNewLLMConfigMutationAtom, deleteNewLLMConfigMutationAtom, - updateNewLLMConfigMutationAtom, } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms"; import { globalNewLLMConfigsAtom, newLLMConfigsAtom, } from "@/atoms/new-llm-config/new-llm-config-query.atoms"; -import { LLMConfigForm, type LLMConfigFormData } from "@/components/shared/llm-config-form"; +import { ModelConfigDialog } from "@/components/shared/model-config-dialog"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertDialog, @@ -39,13 +37,6 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; @@ -69,12 +60,6 @@ function getInitials(name: string): string { export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { const isDesktop = useMediaQuery("(min-width: 768px)"); // Mutations - const { mutateAsync: createConfig, isPending: isCreating } = useAtomValue( - createNewLLMConfigMutationAtom - ); - const { mutateAsync: updateConfig, isPending: isUpdating } = useAtomValue( - updateNewLLMConfigMutationAtom - ); const { mutateAsync: deleteConfig, isPending: isDeleting } = useAtomValue( deleteNewLLMConfigMutationAtom ); @@ -128,29 +113,6 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { const [editingConfig, setEditingConfig] = useState(null); const [configToDelete, setConfigToDelete] = useState(null); - const isSubmitting = isCreating || isUpdating; - - const handleFormSubmit = useCallback( - async (formData: LLMConfigFormData) => { - try { - if (editingConfig) { - const { search_space_id, ...updateData } = formData; - await updateConfig({ - id: editingConfig.id, - data: updateData, - }); - } else { - await createConfig(formData); - } - setIsDialogOpen(false); - setEditingConfig(null); - } catch { - // Error is displayed inside the dialog by the form - } - }, - [editingConfig, createConfig, updateConfig] - ); - const handleDelete = async () => { if (!configToDelete) return; try { @@ -171,11 +133,6 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { setIsDialogOpen(true); }; - const closeDialog = () => { - setIsDialogOpen(false); - setEditingConfig(null); - }; - return (
{/* Header actions */} @@ -457,54 +414,17 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { )} {/* Add/Edit Configuration Dialog */} - !open && closeDialog()}> - e.preventDefault()} - > - - - {editingConfig ? "Edit Configuration" : "Create New Configuration"} - - - {editingConfig - ? "Update your AI model and prompt configuration" - : "Set up a new AI model with custom prompts and citation settings"} - - - - - - + { + setIsDialogOpen(open); + if (!open) setEditingConfig(null); + }} + config={editingConfig} + isGlobal={false} + searchSpaceId={searchSpaceId} + mode={editingConfig ? "edit" : "create"} + /> {/* Delete Confirmation Dialog */} void; + config: NewLLMConfigPublic | GlobalNewLLMConfig | null; + isGlobal: boolean; + searchSpaceId: number; + mode: "create" | "edit" | "view"; +} + +export function ModelConfigDialog({ + open, + onOpenChange, + config, + isGlobal, + searchSpaceId, + mode, +}: ModelConfigDialogProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [scrollPos, setScrollPos] = useState<"top" | "middle" | "bottom">("top"); + const scrollRef = useRef(null); + + 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 { mutateAsync: createConfig } = useAtomValue(createNewLLMConfigMutationAtom); + const { mutateAsync: updateConfig } = useAtomValue(updateNewLLMConfigMutationAtom); + const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom); + + const isAutoMode = config && "is_auto_mode" in config && config.is_auto_mode; + + const getTitle = () => { + if (mode === "create") return "Add New Configuration"; + if (isAutoMode) return "Auto Mode (Fastest)"; + if (isGlobal) return "View Global Configuration"; + return "Edit Configuration"; + }; + + const getSubtitle = () => { + if (mode === "create") return "Set up a new LLM provider for this search space"; + if (isAutoMode) return "Automatically routes requests across providers"; + if (isGlobal) return "Read-only global configuration"; + return "Update your configuration settings"; + }; + + const handleSubmit = useCallback( + async (data: LLMConfigFormData) => { + setIsSubmitting(true); + try { + if (mode === "create") { + const result = await createConfig({ + ...data, + search_space_id: searchSpaceId, + }); + + if (result?.id) { + await updatePreferences({ + search_space_id: searchSpaceId, + data: { + agent_llm_id: result.id, + }, + }); + } + + onOpenChange(false); + } else if (!isGlobal && config) { + await updateConfig({ + id: config.id, + data: { + name: data.name, + description: data.description, + provider: data.provider, + custom_provider: data.custom_provider, + model_name: data.model_name, + api_key: data.api_key, + api_base: data.api_base, + litellm_params: data.litellm_params, + system_instructions: data.system_instructions, + use_default_system_instructions: data.use_default_system_instructions, + citations_enabled: data.citations_enabled, + }, + }); + onOpenChange(false); + } + } catch (error) { + console.error("Failed to save configuration:", error); + } finally { + setIsSubmitting(false); + } + }, + [ + mode, + isGlobal, + config, + searchSpaceId, + createConfig, + updateConfig, + updatePreferences, + onOpenChange, + ] + ); + + const handleUseGlobalConfig = useCallback(async () => { + if (!config || !isGlobal) return; + setIsSubmitting(true); + try { + await updatePreferences({ + search_space_id: searchSpaceId, + data: { + agent_llm_id: config.id, + }, + }); + toast.success(`Now using ${config.name}`); + onOpenChange(false); + } catch (error) { + console.error("Failed to set model:", error); + } finally { + setIsSubmitting(false); + } + }, [config, isGlobal, searchSpaceId, updatePreferences, onOpenChange]); + + return ( + + e.preventDefault()} + > + {getTitle()} + + {/* Header */} +
+
+
+

{getTitle()}

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

{getSubtitle()}

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

+ {config.model_name} +

+ )} +
+
+ + {/* Scrollable content */} +
+ {isAutoMode && ( + + + Auto mode automatically distributes requests across all available LLM + providers to optimize performance and avoid rate limits. + + + )} + + {isGlobal && !isAutoMode && mode !== "create" && ( + + + + Global configurations are read-only. To customize settings, create a new + configuration based on this template. + + + )} + + {mode === "create" ? ( + + ) : isAutoMode && config ? ( +
+
+
+
+ How It Works +
+

{config.description}

+
+ +
+ +
+
+ Key Benefits +
+
+
+ +
+

+ Automatic (Fastest) +

+

+ Distributes requests across all configured LLM providers +

+
+
+
+ +
+

+ Rate Limit Protection +

+

+ Automatically handles rate limits with cooldowns and retries +

+
+
+
+ +
+

+ Automatic Failover +

+

+ Falls back to other providers if one becomes unavailable +

+
+
+
+
+
+
+ ) : isGlobal && config ? ( +
+
+
+
+
+ Configuration Name +
+

{config.name}

+
+ {config.description && ( +
+
+ Description +
+

{config.description}

+
+ )} +
+ +
+ +
+
+
+ Provider +
+

{config.provider}

+
+
+
+ Model +
+

{config.model_name}

+
+
+ +
+ +
+
+
+ Citations +
+ + {config.citations_enabled ? "Enabled" : "Disabled"} + +
+
+ + {config.system_instructions && ( + <> +
+
+
+ System Instructions +
+
+

+ {config.system_instructions} +

+
+
+ + )} +
+
+ ) : config ? ( + + ) : null} +
+ + {/* Fixed footer */} +
+ + {mode === "create" || (!isGlobal && !isAutoMode && config) ? ( + + ) : isAutoMode ? ( + + ) : isGlobal && config ? ( + + ) : null} +
+ +
+ ); +}