From 5b7e5770beb130159cd0a4dd31f7a13f3ebc66ee Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 9 Dec 2025 19:39:25 +0000 Subject: [PATCH] feat: migrate createLLMConfig to jotai mutation atom and add query atoms for LLM configs --- .../llm-config/llm-config-mutation.atoms.ts | 9 +- .../llm-config/llm-config-query.atoms.ts | 31 +++++++ .../components/onboard/setup-llm-step.tsx | 25 +++--- .../settings/model-config-manager.tsx | 87 +++++++++---------- .../contracts/types/llm-config.types.ts | 9 +- surfsense_web/hooks/use-llm-configs.ts | 3 +- 6 files changed, 93 insertions(+), 71 deletions(-) create mode 100644 surfsense_web/atoms/llm-config/llm-config-query.atoms.ts diff --git a/surfsense_web/atoms/llm-config/llm-config-mutation.atoms.ts b/surfsense_web/atoms/llm-config/llm-config-mutation.atoms.ts index 6e0c18d73..c0e6a5886 100644 --- a/surfsense_web/atoms/llm-config/llm-config-mutation.atoms.ts +++ b/surfsense_web/atoms/llm-config/llm-config-mutation.atoms.ts @@ -7,6 +7,7 @@ import type { DeleteLLMConfigRequest, GetLLMConfigsResponse, UpdateLLMPreferencesRequest, + UpdateLLMConfigResponse, } from "@/contracts/types/llm-config.types"; import { llmConfigApiService } from "@/lib/apis/llm-config-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; @@ -44,7 +45,7 @@ export const updateLLMConfigMutationAtom = atomWithMutation((get) => { return llmConfigApiService.updateLLMConfig(request); }, - onSuccess: (_, request: UpdateLLMConfigRequest) => { + onSuccess: (_: UpdateLLMConfigResponse , request: UpdateLLMConfigRequest) => { toast.success("LLM configuration updated successfully"); queryClient.invalidateQueries({ queryKey: cacheKeys.llmConfigs.all(searchSpaceId!), @@ -76,11 +77,7 @@ export const deleteLLMConfigMutationAtom = atomWithMutation((get) => { cacheKeys.llmConfigs.all(searchSpaceId!), (oldData: GetLLMConfigsResponse | undefined) => { if (!oldData) return oldData; - return { - ...oldData, - items: oldData.items.filter((config) => config.id !== request.id), - total: oldData.total - 1, - }; + return oldData.filter((config) => config.id !== request.id); } ); queryClient.invalidateQueries({ diff --git a/surfsense_web/atoms/llm-config/llm-config-query.atoms.ts b/surfsense_web/atoms/llm-config/llm-config-query.atoms.ts new file mode 100644 index 000000000..463e36fa8 --- /dev/null +++ b/surfsense_web/atoms/llm-config/llm-config-query.atoms.ts @@ -0,0 +1,31 @@ +import { atomWithQuery } from "jotai-tanstack-query"; +import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; +import { llmConfigApiService } from "@/lib/apis/llm-config-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; + +export const llmConfigsAtom = atomWithQuery((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + + return { + queryKey: cacheKeys.llmConfigs.all(searchSpaceId!), + enabled: !!searchSpaceId, + staleTime: 5 * 60 * 1000, // 5 minutes + queryFn: async () => { + return llmConfigApiService.getLLMConfigs({ + queryParams: { + search_space_id: searchSpaceId!, + }, + }); + }, + }; +}); + +export const globalLLMConfigsAtom = atomWithQuery(() => { + return { + queryKey: cacheKeys.llmConfigs.global(), + staleTime: 10 * 60 * 1000, // 10 minutes + queryFn: async () => { + return llmConfigApiService.getGlobalLLMConfigs(); + }, + }; +}); diff --git a/surfsense_web/components/onboard/setup-llm-step.tsx b/surfsense_web/components/onboard/setup-llm-step.tsx index 41cc5be99..fc89e6d00 100644 --- a/surfsense_web/components/onboard/setup-llm-step.tsx +++ b/surfsense_web/components/onboard/setup-llm-step.tsx @@ -52,6 +52,9 @@ import { import { cn } from "@/lib/utils"; import InferenceParamsEditor from "../inference-params-editor"; +import { useAtomValue } from "jotai"; +import { createLLMConfigMutationAtom } from "@/atoms/llm-config/llm-config-mutation.atoms"; +import { CreateLLMConfigRequest } from "@/contracts/types/llm-config.types"; interface SetupLLMStepProps { searchSpaceId: number; @@ -97,14 +100,15 @@ export function SetupLLMStep({ onPreferencesUpdated, }: SetupLLMStepProps) { const t = useTranslations("onboard"); - const { llmConfigs, createLLMConfig, deleteLLMConfig } = useLLMConfigs(searchSpaceId); + const { llmConfigs, deleteLLMConfig } = useLLMConfigs(searchSpaceId); + const { mutateAsync : createLLMConfig, isPending : isCreatingLlmConfig } = useAtomValue(createLLMConfigMutationAtom) const { globalConfigs } = useGlobalLLMConfigs(); const { preferences, updatePreferences } = useLLMPreferences(searchSpaceId); const [isAddingNew, setIsAddingNew] = useState(false); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ name: "", - provider: "", + provider: "" as CreateLLMConfigRequest["provider"], // Allow it as Default custom_provider: "", model_name: "", api_key: "", @@ -113,7 +117,6 @@ export function SetupLLMStep({ litellm_params: {}, search_space_id: searchSpaceId, }); - const [isSubmitting, setIsSubmitting] = useState(false); const [modelComboboxOpen, setModelComboboxOpen] = useState(false); const [showProviderForm, setShowProviderForm] = useState(false); @@ -146,14 +149,12 @@ export function SetupLLMStep({ return; } - setIsSubmitting(true); const result = await createLLMConfig(formData); - setIsSubmitting(false); if (result) { setFormData({ name: "", - provider: "", + provider: "" as CreateLLMConfigRequest["provider"], custom_provider: "", model_name: "", api_key: "", @@ -417,7 +418,7 @@ export function SetupLLMStep({ handleInputChange("custom_provider", e.target.value)} required /> @@ -543,7 +544,7 @@ export function SetupLLMStep({ handleInputChange("api_base", e.target.value)} /> {/* Ollama-specific help */} @@ -590,15 +591,15 @@ export function SetupLLMStep({
- diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx index abdde04e3..ac604ee39 100644 --- a/surfsense_web/components/settings/model-config-manager.tsx +++ b/surfsense_web/components/settings/model-config-manager.tsx @@ -60,34 +60,30 @@ import { LANGUAGES } from "@/contracts/enums/languages"; import { getModelsByProvider } from "@/contracts/enums/llm-models"; import { LLM_PROVIDERS } from "@/contracts/enums/llm-providers"; import { - type CreateLLMConfig, - type LLMConfig, useGlobalLLMConfigs, - useLLMConfigs, } from "@/hooks/use-llm-configs"; import { cn } from "@/lib/utils"; import InferenceParamsEditor from "../inference-params-editor"; +import { useAtomValue } from "jotai"; +import { createLLMConfigMutationAtom, deleteLLMConfigMutationAtom, updateLLMConfigMutationAtom } from "@/atoms/llm-config/llm-config-mutation.atoms"; +import { CreateLLMConfigRequest, CreateLLMConfigResponse, LLMConfig, UpdateLLMConfigResponse } from "@/contracts/types/llm-config.types"; +import { llmConfigsAtom } from "@/atoms/llm-config/llm-config-query.atoms"; interface ModelConfigManagerProps { searchSpaceId: number; } export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { - const { - llmConfigs, - loading, - error, - createLLMConfig, - updateLLMConfig, - deleteLLMConfig, - refreshConfigs, - } = useLLMConfigs(searchSpaceId); + const { mutateAsync : createLLMConfig, isPending : isCreatingLLMConfig, error : createLLMConfigError, isError : isCreateLLMConfigError } = useAtomValue(createLLMConfigMutationAtom) + const { mutateAsync : updateLLMConfig, isPending : isUpdatingLLMConfig, error : updateLLMConfigError, isError : isUpdateLLMConfigError} = useAtomValue(updateLLMConfigMutationAtom) + const { mutateAsync : deleteLLMConfig, isPending : isDeletingLLMConfig, error : deleteLLMConfigError, isError : isDeleteLLMConfigError } = useAtomValue(deleteLLMConfigMutationAtom) + const { data : llmConfigs, isFetching : isFetchingLLMConfigs, error : LLMConfigsFetchError, isError : isLLMConfigsFetchError, refetch : refreshConfigs} = useAtomValue(llmConfigsAtom) const { globalConfigs } = useGlobalLLMConfigs(); const [isAddingNew, setIsAddingNew] = useState(false); const [editingConfig, setEditingConfig] = useState(null); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ name: "", - provider: "", + provider: "" as CreateLLMConfigRequest["provider"], // Allow it as Default, custom_provider: "", model_name: "", api_key: "", @@ -96,7 +92,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { litellm_params: {}, search_space_id: searchSpaceId, }); - const [isSubmitting, setIsSubmitting] = useState(false); + const isSubmitting = isCreatingLLMConfig || isUpdatingLLMConfig + const errors = [createLLMConfigError, updateLLMConfigError, deleteLLMConfigError, LLMConfigsFetchError] as Error[] + const isError = Boolean(errors.filter(Boolean).length) const [modelComboboxOpen, setModelComboboxOpen] = useState(false); const [configToDelete, setConfigToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); @@ -118,12 +116,12 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { } }, [editingConfig, searchSpaceId]); - const handleInputChange = (field: keyof CreateLLMConfig, value: string) => { + const handleInputChange = (field: keyof CreateLLMConfigRequest, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }; // Handle provider change with auto-fill API Base URL and reset model / 处理 Provider 变更并自动填充 API Base URL 并重置模型 - const handleProviderChange = (providerValue: string) => { + const handleProviderChange = (providerValue : CreateLLMConfigRequest["provider"]) => { const provider = LLM_PROVIDERS.find((p) => p.value === providerValue); setFormData((prev) => ({ ...prev, @@ -134,6 +132,8 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { })); }; + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.name || !formData.provider || !formData.model_name || !formData.api_key) { @@ -141,23 +141,19 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { return; } - setIsSubmitting(true); - - let result: LLMConfig | null = null; + let result: CreateLLMConfigResponse | UpdateLLMConfigResponse | null = null; if (editingConfig) { // Update existing config - result = await updateLLMConfig(editingConfig.id, formData); + result = await updateLLMConfig({id : editingConfig.id, data : formData}); } else { // Create new config result = await createLLMConfig(formData); } - setIsSubmitting(false); - if (result) { setFormData({ name: "", - provider: "", + provider: "" as CreateLLMConfigRequest["provider"], custom_provider: "", model_name: "", api_key: "", @@ -177,14 +173,11 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { const handleConfirmDelete = async () => { if (!configToDelete) return; - setIsDeleting(true); try { - await deleteLLMConfig(configToDelete.id); - toast.success("Configuration deleted successfully"); + await deleteLLMConfig({id : configToDelete.id}); } catch (error) { toast.error("Failed to delete configuration"); } finally { - setIsDeleting(false); setConfigToDelete(null); } }; @@ -217,26 +210,28 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Error Alert */} - {error && ( + {isError && errors.filter(Boolean).map(err => { + return ( - {error} + {err?.message ?? "Something went wrong"} - )} + ) + }) } {/* Global Configs Info Alert */} - {!loading && !error && globalConfigs.length > 0 && ( + {!isFetchingLLMConfigs && !isError && globalConfigs.length > 0 && ( @@ -250,7 +245,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { )} {/* Loading State */} - {loading && ( + {isFetchingLLMConfigs && (
@@ -262,14 +257,14 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { )} {/* Stats Overview */} - {!loading && !error && ( + {!isFetchingLLMConfigs && !isError&& (
-

{llmConfigs.length}

+

{llmConfigs?.length}

Total Configs

@@ -285,7 +280,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {

- {new Set(llmConfigs.map((c) => c.provider)).size} + {new Set(llmConfigs?.map((c) => c.provider)).size}

Providers

@@ -314,7 +309,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { )} {/* Configuration Management */} - {!loading && !error && ( + {!isFetchingLLMConfigs && !isError && (
@@ -329,7 +324,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
- {llmConfigs.length === 0 ? ( + {llmConfigs?.length === 0 ? (
@@ -350,7 +345,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { ) : (
- {llmConfigs.map((config) => { + {llmConfigs?.map((config) => { const providerInfo = getProviderInfo(config.provider); return ( handleInputChange("custom_provider", e.target.value)} required /> @@ -683,7 +678,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { handleInputChange("api_base", e.target.value)} /> {selectedProvider?.apiBase && formData.api_base === selectedProvider.apiBase && ( @@ -765,7 +760,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { setEditingConfig(null); setFormData({ name: "", - provider: "", + provider: "" as LLMConfig["provider"], custom_provider: "", model_name: "", api_key: "", diff --git a/surfsense_web/contracts/types/llm-config.types.ts b/surfsense_web/contracts/types/llm-config.types.ts index 1cfba6e4c..870ba8473 100644 --- a/surfsense_web/contracts/types/llm-config.types.ts +++ b/surfsense_web/contracts/types/llm-config.types.ts @@ -38,7 +38,7 @@ export const llmConfig = z.object({ id: z.number(), name: z.string().max(100), provider: liteLLMProviderEnum, - custom_provider: z.string().max(100).nullable().optional(), + custom_provider: z.string().nullable().optional(), model_name: z.string().max(100), api_key: z.string(), api_base: z.string().nullable().optional(), @@ -46,7 +46,7 @@ export const llmConfig = z.object({ litellm_params: z.record(z.string(), z.any()).nullable().optional(), search_space_id: z.number(), created_at: z.string(), - updated_at: z.string(), + updated_at: z.string().optional(), }); export const globalLLMConfig = llmConfig @@ -98,10 +98,7 @@ export const getLLMConfigsRequest = z.object({ .nullish(), }); -export const getLLMConfigsResponse = z.object({ - items: z.array(llmConfig), - total: z.number(), -}); +export const getLLMConfigsResponse = z.array(llmConfig); /** * Get LLM config by ID diff --git a/surfsense_web/hooks/use-llm-configs.ts b/surfsense_web/hooks/use-llm-configs.ts index 7619cc3e4..8d0af2aa7 100644 --- a/surfsense_web/hooks/use-llm-configs.ts +++ b/surfsense_web/hooks/use-llm-configs.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { toast } from "sonner"; import { authenticatedFetch } from "@/lib/auth-utils"; +import { UpdateLLMConfigRequest } from "@/contracts/types/llm-config.types"; export interface LLMConfig { id: number; @@ -136,7 +137,7 @@ export function useLLMConfigs(searchSpaceId: number | null) { const updateLLMConfig = async ( id: number, - config: UpdateLLMConfig + config: UpdateLLMConfigRequest["data"] ): Promise => { try { const response = await authenticatedFetch(