feat(ui): migrate MCPSemanticFilterSettings + TestPanel to shadcn

- MCPSemanticFilterTestPanel: antd Alert/Button/Card/Input/Space/Tabs/
  Typography + @ant-design/icons CodeOutlined/PlayCircleOutlined \u2192
  shadcn Alert/Button/Card/Label/Textarea/Tabs + lucide Code/PlayCircle.
  Inline disabled/selected-tools/api-usage panels. Added to
  .eslintrc.json overrides for the amber/emerald categorical Alert
  palette.
- MCPSemanticFilterSettings: antd Form/InputNumber/Slider/Select/Switch/
  Row/Col/Tooltip/Typography + @ant-design/icons QuestionCircle/
  CheckCircle/Save \u2192 shadcn Card/Input/Select/Slider/Switch/Alert/
  Tooltip + react-hook-form (form.reset preserves 'dirty' semantics;
  form.formState.isDirty gates the Save button). Shadcn Slider returns
  an array; adapter passes [value] in, extracts [v] out. Tick labels
  rendered as a flex row beneath the track.

Gates: TS \u2713 | Vitest 33/33 \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 09:43:57 +00:00
parent 2dae5d9372
commit 28e1cecc7e
No known key found for this signature in database
3 changed files with 393 additions and 322 deletions

View file

@ -34,7 +34,8 @@
"src/components/SCIM.tsx",
"src/components/logging_settings_view.tsx",
"src/components/model_group_alias_settings.tsx",
"src/components/cache_dashboard.tsx"
"src/components/cache_dashboard.tsx",
"src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx"
],
"rules": {
"litellm-ui/no-banned-ui-imports": "off",

View file

@ -5,51 +5,96 @@ import { useUpdateMCPSemanticFilterSettings } from "@/app/(dashboard)/hooks/mcpS
import NotificationManager from "@/components/molecules/notifications_manager";
import {
Alert,
Button,
Card,
Col,
Form,
InputNumber,
Row,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
Skeleton,
Slider,
Space,
Switch,
Typography,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
} from "antd";
import { QuestionCircleOutlined, CheckCircleOutlined, SaveOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react";
import { fetchAvailableModels, ModelGroup } from "@/components/playground/llm_calls/fetch_models";
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CheckCircle, HelpCircle, Save } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import {
fetchAvailableModels,
ModelGroup,
} from "@/components/playground/llm_calls/fetch_models";
import MCPSemanticFilterTestPanel from "./MCPSemanticFilterTestPanel";
import { getCurlCommand, runSemanticFilterTest, TestResult } from "./semanticFilterTestUtils";
import {
getCurlCommand,
runSemanticFilterTest,
TestResult,
} from "./semanticFilterTestUtils";
interface MCPSemanticFilterSettingsProps {
accessToken: string | null;
}
export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFilterSettingsProps) {
interface FormValues {
enabled: boolean;
embedding_model: string;
top_k: number;
similarity_threshold: number;
}
function HelpTip({ children }: { children: React.ReactNode }) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="inline-block ml-1 h-3 w-3 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">{children}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
export default function MCPSemanticFilterSettings({
accessToken,
}: MCPSemanticFilterSettingsProps) {
const { data, isLoading, isError, error } = useMCPSemanticFilterSettings();
const {
mutate: updateSettings,
isPending: isUpdating,
error: updateError,
} = useUpdateMCPSemanticFilterSettings(accessToken || "");
const [form] = Form.useForm();
const form = useForm<FormValues>({
defaultValues: {
enabled: false,
embedding_model: "text-embedding-3-small",
top_k: 10,
similarity_threshold: 0.3,
},
mode: "onSubmit",
});
const [saveSuccess, setSaveSuccess] = useState(false);
const [isDirty, setIsDirty] = useState(false);
const [embeddingModels, setEmbeddingModels] = useState<ModelGroup[]>([]);
const [loadingModels, setLoadingModels] = useState(true);
// Test section state
const [testQuery, setTestQuery] = useState("");
const [testModel, setTestModel] = useState<string>("gpt-4o");
const [testResult, setTestResult] = useState<TestResult | null>(null);
const [isTesting, setIsTesting] = useState(false);
const schema = data?.field_schema;
const values = data?.values ?? {};
const values = useMemo(() => data?.values ?? {}, [data?.values]);
useEffect(() => {
const loadEmbeddingModels = async () => {
@ -57,56 +102,48 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi
try {
setLoadingModels(true);
const models = await fetchAvailableModels(accessToken);
const embeddingOnly = models.filter((model) => model.mode === "embedding");
setEmbeddingModels(embeddingOnly);
setEmbeddingModels(models.filter((m) => m.mode === "embedding"));
} catch (error) {
console.error("Error fetching embedding models:", error);
} finally {
setLoadingModels(false);
}
};
loadEmbeddingModels();
}, [accessToken]);
useEffect(() => {
if (values) {
form.setFieldsValue({
form.reset({
enabled: values.enabled ?? false,
embedding_model: values.embedding_model ?? "text-embedding-3-small",
top_k: values.top_k ?? 10,
similarity_threshold: values.similarity_threshold ?? 0.3,
});
setIsDirty(false);
}
}, [values, form]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [values]);
const handleSave = async () => {
try {
const formValues = await form.validateFields();
updateSettings(formValues, {
onSuccess: () => {
setIsDirty(false);
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
NotificationManager.success(
"Settings updated successfully. Changes will be applied across all pods within 10 seconds."
);
},
onError: (error) => {
NotificationManager.fromBackend(error);
},
});
} catch (error) {
console.error("Form validation failed:", error);
}
};
const isDirty = form.formState.isDirty;
const handleSave = form.handleSubmit((formValues) => {
updateSettings(formValues, {
onSuccess: () => {
form.reset(formValues); // clears dirty state
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
NotificationManager.success(
"Settings updated successfully. Changes will be applied across all pods within 10 seconds.",
);
},
onError: (err) => {
NotificationManager.fromBackend(err);
},
});
});
const handleTest = async () => {
if (!accessToken) {
return;
}
if (!accessToken) return;
await runSemanticFilterTest({
accessToken,
testModel,
@ -118,190 +155,222 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi
if (!accessToken) {
return (
<div className="p-6 text-center text-gray-500">
<div className="p-6 text-center text-muted-foreground">
Please log in to configure semantic filter settings.
</div>
);
}
return (
<div style={{ width: "100%" }}>
<div className="w-full">
{isLoading ? (
<Skeleton active />
<Skeleton className="h-32 w-full" />
) : isError ? (
<Alert
type="error"
message="Could not load MCP Semantic Filter settings"
description={error instanceof Error ? error.message : undefined}
style={{ marginBottom: 24 }}
/>
<Alert variant="destructive" className="mb-6">
<AlertTitle>Could not load MCP Semantic Filter settings</AlertTitle>
{error instanceof Error && (
<AlertDescription>{error.message}</AlertDescription>
)}
</Alert>
) : (
<>
<Alert
type="info"
message="Semantic Tool Filtering"
description="Filter MCP tools semantically based on query relevance. This reduces context window size and improves tool selection accuracy. Click 'Save Settings' to apply changes across all pods (takes effect within 10 seconds)."
showIcon
style={{ marginBottom: 24 }}
/>
<Alert className="mb-6">
<AlertTitle>Semantic Tool Filtering</AlertTitle>
<AlertDescription>
Filter MCP tools semantically based on query relevance. This
reduces context window size and improves tool selection accuracy.
Click &apos;Save Settings&apos; to apply changes across all pods
(takes effect within 10 seconds).
</AlertDescription>
</Alert>
{saveSuccess && (
<Alert
type="success"
message="Settings saved successfully"
icon={<CheckCircleOutlined />}
showIcon
closable
style={{ marginBottom: 16 }}
/>
<Alert className="mb-4 border-emerald-300 bg-emerald-50 text-emerald-900 dark:bg-emerald-950/30 dark:text-emerald-200">
<CheckCircle className="h-4 w-4" />
<AlertTitle>Settings saved successfully</AlertTitle>
</Alert>
)}
{updateError && (
<Alert
type="error"
message="Could not update settings"
description={
updateError instanceof Error ? updateError.message : undefined
}
style={{ marginBottom: 16 }}
/>
<Alert variant="destructive" className="mb-4">
<AlertTitle>Could not update settings</AlertTitle>
{updateError instanceof Error && (
<AlertDescription>{updateError.message}</AlertDescription>
)}
</Alert>
)}
<Row gutter={24}>
{/* Left Column - Settings */}
<Col xs={24} lg={12}>
<Form
form={form}
layout="vertical"
disabled={isUpdating}
onValuesChange={() => {
setIsDirty(true);
}}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<FormProvider {...form}>
<form
onSubmit={handleSave}
className="space-y-4"
>
<Card style={{ marginBottom: 16 }}>
<Form.Item
name="enabled"
label={
<Space>
<Typography.Text strong>Enable Semantic Filtering</Typography.Text>
<Tooltip title="When enabled, only the most relevant MCP tools will be included in requests based on semantic similarity">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
valuePropName="checked"
>
<Switch disabled={isUpdating} />
</Form.Item>
<Typography.Text type="secondary" style={{ display: "block", marginTop: -16, marginBottom: 16 }}>
<Card className="p-4">
<div className="flex items-center justify-between">
<Label>
<span className="font-semibold">
Enable Semantic Filtering
</span>
<HelpTip>
When enabled, only the most relevant MCP tools will be
included in requests based on semantic similarity
</HelpTip>
</Label>
<Controller
control={form.control}
name="enabled"
render={({ field }) => (
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={isUpdating}
/>
)}
/>
</div>
<p className="text-sm text-muted-foreground mt-2">
{schema?.properties?.enabled?.description}
</Typography.Text>
</p>
</Card>
<Card title="Configuration" style={{ marginBottom: 16 }}>
<Form.Item
name="embedding_model"
label={
<Space>
<Typography.Text strong>Embedding Model</Typography.Text>
<Tooltip title="The model used to generate embeddings for semantic matching">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
>
<Select
options={embeddingModels.map((model) => ({
label: model.model_group,
value: model.model_group,
}))}
placeholder={loadingModels ? "Loading models..." : "Select embedding model"}
showSearch
disabled={isUpdating || loadingModels}
loading={loadingModels}
notFoundContent={
loadingModels ? "Loading..." : "No embedding models available"
}
/>
</Form.Item>
<Card className="p-4">
<h4 className="text-base font-semibold mb-4">
Configuration
</h4>
<Form.Item
name="top_k"
label={
<Space>
<Typography.Text strong>Top K Results</Typography.Text>
<Tooltip title="Maximum number of tools to return after filtering">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
>
<InputNumber
min={1}
max={100}
style={{ width: "100%" }}
disabled={isUpdating}
/>
</Form.Item>
<div className="space-y-4">
<div className="space-y-2">
<Label>
<span className="font-semibold">Embedding Model</span>
<HelpTip>
The model used to generate embeddings for semantic
matching
</HelpTip>
</Label>
<Controller
control={form.control}
name="embedding_model"
render={({ field }) => (
<Select
value={field.value ?? ""}
onValueChange={(v) => field.onChange(v)}
disabled={isUpdating || loadingModels}
>
<SelectTrigger>
<SelectValue
placeholder={
loadingModels
? "Loading models..."
: "Select embedding model"
}
/>
</SelectTrigger>
<SelectContent>
{embeddingModels.length === 0 ? (
<div className="py-2 px-3 text-sm text-muted-foreground">
{loadingModels
? "Loading..."
: "No embedding models available"}
</div>
) : (
embeddingModels.map((model) => (
<SelectItem
key={model.model_group}
value={model.model_group}
>
{model.model_group}
</SelectItem>
))
)}
</SelectContent>
</Select>
)}
/>
</div>
<Form.Item
name="similarity_threshold"
label={
<Space>
<Typography.Text strong>Similarity Threshold</Typography.Text>
<Tooltip title="Minimum similarity score (0-1) for a tool to be included">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
>
<Slider
min={0}
max={1}
step={0.05}
marks={{
0: "0.0",
0.3: "0.3",
0.5: "0.5",
0.7: "0.7",
1: "1.0",
}}
disabled={isUpdating}
/>
</Form.Item>
<div className="space-y-2">
<Label htmlFor="top_k">
<span className="font-semibold">Top K Results</span>
<HelpTip>
Maximum number of tools to return after filtering
</HelpTip>
</Label>
<Input
id="top_k"
type="number"
min={1}
max={100}
disabled={isUpdating}
{...form.register("top_k", { valueAsNumber: true })}
/>
</div>
<div className="space-y-2">
<Label>
<span className="font-semibold">
Similarity Threshold
</span>
<HelpTip>
Minimum similarity score (0-1) for a tool to be
included
</HelpTip>
</Label>
<Controller
control={form.control}
name="similarity_threshold"
render={({ field }) => (
<div>
<Slider
value={[field.value]}
onValueChange={([v]) => field.onChange(v)}
min={0}
max={1}
step={0.05}
disabled={isUpdating}
/>
<div className="flex justify-between text-xs text-muted-foreground mt-2">
<span>0.0</span>
<span>0.3</span>
<span>0.5</span>
<span>0.7</span>
<span>1.0</span>
</div>
<div className="text-right text-sm text-muted-foreground mt-1">
Current: {Number(field.value).toFixed(2)}
</div>
</div>
)}
/>
</div>
</div>
</Card>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
<div className="flex justify-end">
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
loading={isUpdating}
disabled={!isDirty}
type="submit"
disabled={!isDirty || isUpdating}
>
Save Settings
<Save className="h-4 w-4" />
{isUpdating ? "Saving…" : "Save Settings"}
</Button>
</div>
</Form>
</Col>
</form>
</FormProvider>
{/* Right Column - Test Configuration */}
<Col xs={24} lg={12}>
<MCPSemanticFilterTestPanel
accessToken={accessToken}
testQuery={testQuery}
setTestQuery={setTestQuery}
testModel={testModel}
setTestModel={setTestModel}
isTesting={isTesting}
onTest={handleTest}
filterEnabled={!!values.enabled}
testResult={testResult}
curlCommand={getCurlCommand(testModel, testQuery)}
/>
</Col>
</Row>
<MCPSemanticFilterTestPanel
accessToken={accessToken}
testQuery={testQuery}
setTestQuery={setTestQuery}
testModel={testModel}
setTestModel={setTestModel}
isTesting={isTesting}
onTest={handleTest}
filterEnabled={!!values.enabled}
testResult={testResult}
curlCommand={getCurlCommand(testModel, testQuery)}
/>
</div>
</>
)}
</div>

View file

@ -1,5 +1,19 @@
import { CodeOutlined, PlayCircleOutlined } from "@ant-design/icons";
import { Alert, Button, Card, Input, Space, Tabs, Typography } from "antd";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { Code, PlayCircle } from "lucide-react";
import ModelSelector from "@/components/common_components/ModelSelector";
import { TestResult } from "./semanticFilterTestUtils";
@ -29,136 +43,123 @@ export default function MCPSemanticFilterTestPanel({
curlCommand,
}: MCPSemanticFilterTestPanelProps) {
return (
<Card title="Test Configuration" style={{ marginBottom: 16 }}>
<Tabs
defaultActiveKey="test"
items={[
{
key: "test",
label: "Test",
children: (
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div>
<Typography.Text strong style={{ display: "block", marginBottom: 8 }}>
<PlayCircleOutlined /> Test Query
</Typography.Text>
<Input.TextArea
placeholder="Enter a test query to see which tools would be selected..."
value={testQuery}
onChange={(e) => setTestQuery(e.target.value)}
rows={4}
disabled={isTesting}
/>
</div>
<div>
<ModelSelector
accessToken={accessToken || ""}
value={testModel}
onChange={setTestModel}
disabled={isTesting}
showLabel={true}
labelText="Select Model"
/>
</div>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={onTest}
loading={isTesting}
disabled={!testQuery || !testModel || !filterEnabled}
block
>
Test Filter
</Button>
{!filterEnabled && (
<Alert
type="warning"
message="Semantic filtering is disabled"
description="Enable semantic filtering and save settings to test the filter."
showIcon
/>
)}
{testResult && (
<Card className="mb-4 p-6">
<h4 className="text-base font-semibold mb-4">Test Configuration</h4>
<Tabs defaultValue="test">
<TabsList>
<TabsTrigger value="test">Test</TabsTrigger>
<TabsTrigger value="api">API Usage</TabsTrigger>
</TabsList>
<TabsContent value="test">
<div className="space-y-4">
<div>
<Typography.Title level={5}>Results</Typography.Title>
<Alert
type="success"
message={`${testResult.selectedTools} tools selected`}
description={`Filtered from ${testResult.totalTools} available tools`}
showIcon
style={{ marginBottom: 16 }}
<Label className="flex items-center gap-1 mb-2">
<PlayCircle className="h-4 w-4" />
Test Query
</Label>
<Textarea
placeholder="Enter a test query to see which tools would be selected..."
value={testQuery}
onChange={(e) => setTestQuery(e.target.value)}
rows={4}
disabled={isTesting}
/>
<div>
<Typography.Text strong style={{ display: "block", marginBottom: 8 }}>
Selected Tools:
</Typography.Text>
<ul style={{ paddingLeft: 20, margin: 0 }}>
{testResult.tools.map((tool, index) => (
<li key={index} style={{ marginBottom: 4 }}>
<Typography.Text>{tool}</Typography.Text>
</li>
))}
</ul>
</div>
</div>
)}
</Space>
),
},
{
key: "api",
label: "API Usage",
children: (
<div>
<ModelSelector
accessToken={accessToken || ""}
value={testModel}
onChange={setTestModel}
disabled={isTesting}
showLabel={true}
labelText="Select Model"
/>
</div>
<Button
onClick={onTest}
disabled={isTesting || !testQuery || !testModel || !filterEnabled}
className="w-full"
>
<PlayCircle className="h-4 w-4" />
{isTesting ? "Testing…" : "Test Filter"}
</Button>
{!filterEnabled && (
<Alert className="border-amber-300 bg-amber-50 text-amber-900 dark:bg-amber-950/30 dark:text-amber-200">
<AlertTitle>Semantic filtering is disabled</AlertTitle>
<AlertDescription>
Enable semantic filtering and save settings to test the
filter.
</AlertDescription>
</Alert>
)}
{testResult && (
<div>
<Space style={{ marginBottom: 8 }}>
<CodeOutlined />
<Typography.Text strong>API Usage</Typography.Text>
</Space>
<Typography.Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
Use this curl command to test the semantic filter with your current configuration.
</Typography.Text>
<Typography.Text strong style={{ display: "block", marginBottom: 8 }}>
<h5 className="text-base font-semibold mb-3">Results</h5>
<Alert className="mb-4 border-emerald-300 bg-emerald-50 text-emerald-900 dark:bg-emerald-950/30 dark:text-emerald-200">
<AlertTitle>
{testResult.selectedTools} tools selected
</AlertTitle>
<AlertDescription>
Filtered from {testResult.totalTools} available tools
</AlertDescription>
</Alert>
<div>
<span className="font-semibold block mb-2">
Selected Tools:
</span>
<ul className="pl-5 m-0 space-y-1">
{testResult.tools.map((tool, index) => (
<li key={index}>
<span>{tool}</span>
</li>
))}
</ul>
</div>
</div>
)}
</div>
</TabsContent>
<TabsContent value="api">
<div>
<div className="flex items-center gap-2 mb-2">
<Code className="h-4 w-4" />
<span className="font-semibold">API Usage</span>
</div>
<p className="text-sm text-muted-foreground mb-2">
Use this curl command to test the semantic filter with your
current configuration.
</p>
<span className="font-semibold block mb-2">
Response headers to check:
</Typography.Text>
<ul style={{ paddingLeft: 20, margin: "0 0 12px 0" }}>
</span>
<ul className="pl-5 mb-3 space-y-2">
<li>
<Typography.Text>
<span>
x-litellm-semantic-filter: shows total tools → selected tools
</Typography.Text>
<Typography.Text type="secondary" style={{ display: "block" }}>
</span>
<span className="text-muted-foreground block text-xs">
Example: 10→3
</Typography.Text>
</span>
</li>
<li>
<Typography.Text>
<span>
x-litellm-semantic-filter-tools: CSV of selected tool names
</Typography.Text>
<Typography.Text type="secondary" style={{ display: "block" }}>
</span>
<span className="text-muted-foreground block text-xs">
Example: wikipedia-fetch,github-search,slack-post
</Typography.Text>
</span>
</li>
</ul>
<pre
style={{
background: "#f5f5f5",
padding: 12,
borderRadius: 4,
overflow: "auto",
fontSize: 12,
margin: 0,
}}
>
<pre className="bg-muted border border-border p-3 rounded-md overflow-auto text-xs m-0">
{curlCommand}
</pre>
</div>
),
},
]}
/>
</div>
</TabsContent>
</Tabs>
</Card>
);
}