Add model setting modal

This commit is contained in:
yuneng-jiang 2026-02-18 18:01:09 -08:00
parent 2c6095bdf2
commit b0973b3ac8
3 changed files with 234 additions and 55 deletions

View file

@ -0,0 +1,59 @@
import { useMutation, UseMutationResult } from "@tanstack/react-query";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import useAuthorized from "../useAuthorized";
export interface StoreModelInDBParams {
store_model_in_db: boolean;
}
export interface StoreModelInDBResponse {
message: string;
}
const performStoreModelInDB = async (
accessToken: string,
params: StoreModelInDBParams
): Promise<StoreModelInDBResponse> => {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl ? `${proxyBaseUrl}/config/update` : `/config/update`;
const response = await fetch(url, {
method: "POST",
headers: {
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
general_settings: {
store_model_in_db: params.store_model_in_db,
},
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage =
errorData?.error?.message || errorData?.message || errorData?.detail || "Failed to update model storage settings";
throw new Error(errorMessage);
}
const data = await response.json();
return data;
};
export const useStoreModelInDB = (): UseMutationResult<
StoreModelInDBResponse,
Error,
StoreModelInDBParams
> => {
const { accessToken } = useAuthorized();
return useMutation<StoreModelInDBResponse, Error, StoreModelInDBParams>({
mutationFn: async (params: StoreModelInDBParams) => {
if (!accessToken) {
throw new Error("Access token is required");
}
return await performStoreModelInDB(accessToken, params);
},
});
};

View file

@ -5,10 +5,11 @@ import { Team } from "@/components/key_team_helpers/key_list";
import { AllModelsDataTable } from "@/components/model_dashboard/all_models_table";
import { columns } from "@/components/molecules/models/columns";
import { getDisplayModelName } from "@/components/view_model/model_name_display";
import { InfoCircleOutlined } from "@ant-design/icons";
import { InfoCircleOutlined, SettingOutlined } from "@ant-design/icons";
import { PaginationState, SortingState } from "@tanstack/react-table";
import { Grid, TabPanel } from "@tremor/react";
import { Badge, Select, Skeleton, Space, Typography } from "antd";
import { Badge, Button, Select, Skeleton, Space, Typography } from "antd";
import ModelSettingsModal from "@/components/model_dashboard/ModelSettingsModal/ModelSettingsModal";
import debounce from "lodash/debounce";
import { useEffect, useMemo, useState } from "react";
import { useModelsInfo } from "../../hooks/models/useModels";
@ -51,6 +52,7 @@ const AllModelsTab = ({
pageSize: 50,
});
const [sorting, setSorting] = useState<SortingState>([]);
const [isModelSettingsModalVisible, setIsModelSettingsModalVisible] = useState(false);
// Debounce search input
const debouncedUpdateSearch = useMemo(
@ -326,62 +328,71 @@ const AllModelsTab = ({
<div className="border-b px-6 py-4">
<div className="flex flex-col space-y-4">
{/* Search and Filter Controls */}
<div className="flex flex-wrap items-center gap-3">
{/* Model Name Search */}
<div className="relative w-64">
<input
type="text"
placeholder="Search model names..."
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={modelNameSearch}
onChange={(e) => setModelNameSearch(e.target.value)}
/>
<svg
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
<div className="flex items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-3">
{/* Model Name Search */}
<div className="relative w-64">
<input
type="text"
placeholder="Search model names..."
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={modelNameSearch}
onChange={(e) => setModelNameSearch(e.target.value)}
/>
</svg>
<svg
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
{/* Filter Button */}
<button
className={`px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2 ${showFilters ? "bg-gray-100" : ""}`}
onClick={() => setShowFilters(!showFilters)}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
/>
</svg>
Filters
</button>
{/* Reset Filters Button */}
<button
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
onClick={resetFilters}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Reset Filters
</button>
</div>
{/* Filter Button */}
<button
className={`px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2 ${showFilters ? "bg-gray-100" : ""}`}
onClick={() => setShowFilters(!showFilters)}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
/>
</svg>
Filters
</button>
{/* Reset Filters Button */}
<button
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
onClick={resetFilters}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Reset Filters
</button>
{/* Model Settings Button */}
<Button
icon={<SettingOutlined />}
onClick={() => setIsModelSettingsModalVisible(true)}
title="Model Settings"
/>
</div>
{/* Additional Filters */}
@ -505,6 +516,11 @@ const AllModelsTab = ({
</div>
</div>
</Grid>
<ModelSettingsModal
isVisible={isModelSettingsModalVisible}
onCancel={() => setIsModelSettingsModalVisible(false)}
onSuccess={() => setIsModelSettingsModalVisible(false)}
/>
</TabPanel>
);
};

View file

@ -0,0 +1,104 @@
"use client";
import { ConfigType, useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig";
import { StoreModelInDBParams, useStoreModelInDB } from "@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { parseErrorMessage } from "@/components/shared/errorUtils";
import { Button, Form, Modal, Skeleton, Space, Switch, Typography } from "antd";
import React, { useEffect, useMemo } from "react";
interface ModelSettingsModalProps {
isVisible: boolean;
onCancel: () => void;
onSuccess?: () => void;
}
const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({ isVisible, onCancel, onSuccess }) => {
const [form] = Form.useForm();
const { mutateAsync, isPending } = useStoreModelInDB();
const { data: proxyConfigData, isLoading: isLoadingConfig, refetch } = useProxyConfig(ConfigType.GENERAL_SETTINGS);
// Refetch config when modal opens to ensure we have the latest values
useEffect(() => {
if (isVisible) {
refetch();
}
}, [isVisible, refetch]);
// Compute initial values from fetched config data
const initialValues = useMemo(() => {
if (!proxyConfigData) {
return {
store_model_in_db: false,
};
}
const storeModelField = proxyConfigData.find(field => field.field_name === 'store_model_in_db');
return {
store_model_in_db: storeModelField?.field_value ?? false,
};
}, [proxyConfigData]);
const handleFormSubmit = async (formValues: StoreModelInDBParams) => {
try {
await mutateAsync(formValues, {
onSuccess: () => {
NotificationsManager.success("Model storage settings updated successfully");
refetch();
onSuccess?.();
},
onError: (error) => {
NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error));
},
});
} catch (error) {
NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error));
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title={<Typography.Title level={5}>Model Settings</Typography.Title>}
open={isVisible}
footer={
<Space>
<Button onClick={handleCancel} disabled={isPending || isLoadingConfig}>
Cancel
</Button>
<Button type="primary" loading={isPending} disabled={isLoadingConfig} onClick={() => form.submit()}>
{isPending ? "Saving..." : "Save Settings"}
</Button>
</Space>
}
onCancel={handleCancel}
>
<Form
key={proxyConfigData ? JSON.stringify(initialValues) : 'loading'}
form={form}
layout="horizontal"
onFinish={handleFormSubmit}
initialValues={initialValues}
>
<Form.Item
label="Store Model in DB"
name="store_model_in_db"
tooltip={
proxyConfigData?.find(f => f.field_name === 'store_model_in_db')?.field_description ||
"If enabled, models and config are stored in and loaded from the database."
}
valuePropName="checked"
>
{isLoadingConfig ? <Skeleton.Input active block /> : <Switch />}
</Form.Item>
</Form>
</Modal>
);
};
export default ModelSettingsModal;