feat: migrate createLLMConfig to jotai mutation atom and add query atoms for LLM configs

This commit is contained in:
CREDO23 2025-12-09 19:39:25 +00:00
parent 0c0491cd49
commit 5b7e5770be
6 changed files with 93 additions and 71 deletions

View file

@ -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<LLMConfig | null>(null);
const [formData, setFormData] = useState<CreateLLMConfig>({
const [formData, setFormData] = useState<CreateLLMConfigRequest>({
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<LLMConfig | null>(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) {
<Button
variant="outline"
size="sm"
onClick={refreshConfigs}
disabled={loading}
onClick={() => refreshConfigs()}
disabled={isFetchingLLMConfigs}
className="flex items-center gap-2"
>
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
<RefreshCw className={`h-4 w-4 ${isFetchingLLMConfigs ? "animate-spin" : ""}`} />
Refresh
</Button>
</div>
</div>
{/* Error Alert */}
{error && (
{isError && errors.filter(Boolean).map(err => {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
<AlertDescription>{err?.message ?? "Something went wrong"}</AlertDescription>
</Alert>
)}
)
}) }
{/* Global Configs Info Alert */}
{!loading && !error && globalConfigs.length > 0 && (
{!isFetchingLLMConfigs && !isError && globalConfigs.length > 0 && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
@ -250,7 +245,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
)}
{/* Loading State */}
{loading && (
{isFetchingLLMConfigs && (
<Card>
<CardContent className="flex items-center justify-center py-12">
<div className="flex items-center gap-2 text-muted-foreground">
@ -262,14 +257,14 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
)}
{/* Stats Overview */}
{!loading && !error && (
{!isFetchingLLMConfigs && !isError&& (
<div className="grid gap-3 grid-cols-3">
<Card className="overflow-hidden">
<div className="h-1 bg-blue-500" />
<CardContent className="p-4">
<div className="flex items-start justify-between gap-2">
<div className="space-y-1 min-w-0">
<p className="text-2xl font-bold tracking-tight">{llmConfigs.length}</p>
<p className="text-2xl font-bold tracking-tight">{llmConfigs?.length}</p>
<p className="text-xs font-medium text-muted-foreground">Total Configs</p>
</div>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-blue-500/10">
@ -285,7 +280,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
<div className="flex items-start justify-between gap-2">
<div className="space-y-1 min-w-0">
<p className="text-2xl font-bold tracking-tight">
{new Set(llmConfigs.map((c) => c.provider)).size}
{new Set(llmConfigs?.map((c) => c.provider)).size}
</p>
<p className="text-xs font-medium text-muted-foreground">Providers</p>
</div>
@ -314,7 +309,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
)}
{/* Configuration Management */}
{!loading && !error && (
{!isFetchingLLMConfigs && !isError && (
<div className="space-y-6">
<div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div>
@ -329,7 +324,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
</Button>
</div>
{llmConfigs.length === 0 ? (
{llmConfigs?.length === 0 ? (
<Card className="border-dashed border-2 border-muted-foreground/25">
<CardContent className="flex flex-col items-center justify-center py-16 text-center">
<div className="rounded-full bg-muted p-4 mb-6">
@ -350,7 +345,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
) : (
<div className="grid gap-4">
<AnimatePresence>
{llmConfigs.map((config) => {
{llmConfigs?.map((config) => {
const providerInfo = getProviderInfo(config.provider);
return (
<motion.div
@ -466,7 +461,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
setEditingConfig(null);
setFormData({
name: "",
provider: "",
provider: "" as LLMConfig["provider"],
custom_provider: "",
model_name: "",
api_key: "",
@ -538,7 +533,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
<Input
id="custom_provider"
placeholder="e.g., my-custom-provider"
value={formData.custom_provider}
value={formData.custom_provider ?? ""}
onChange={(e) => handleInputChange("custom_provider", e.target.value)}
required
/>
@ -683,7 +678,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
<Input
id="api_base"
placeholder={selectedProvider?.apiBase || "e.g., https://api.openai.com/v1"}
value={formData.api_base}
value={formData.api_base ?? ""}
onChange={(e) => 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: "",