From c61dea5af9178e8983956b4201b0f781ea33300e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 20 Feb 2026 18:42:11 -0800 Subject: [PATCH] UI: Redesign guardrail creation form with vertical stepper (#21727) * ui: redesign guardrail creation form with inline vertical stepper Replace horizontal Ant Design Steps with an inline vertical stepper. Completed steps collapse to a single line, active step expands. Switch to Tremor buttons, rename steps for clarity. * ui: rename Content Categories to Blocked topics and fix overflow Update heading and description text, add flexWrap to prevent text from going off-screen, fix YAML preview overflow with pre-wrap and word-break. * feat: support explicit display_name in content filter category YAML Check for a display_name field before auto-generating from category_name. Lets categories have human-friendly names without changing their API identifier. * fix: update denied_financial_advice display name Add display_name field so it shows as "Denied Financial / Investment Advice" in the UI. --- .../litellm_content_filter/patterns.py | 8 +- .../guardrails/add_guardrail_form.tsx | 225 ++++++++++++------ .../ContentCategoryConfiguration.tsx | 13 +- 3 files changed, 160 insertions(+), 86 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/patterns.py b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/patterns.py index fc799c411f8..27e554a1025 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/patterns.py +++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/patterns.py @@ -162,11 +162,9 @@ def get_available_content_categories() -> List[Dict[str, str]]: category_data = yaml.safe_load(f) if category_data and "category_name" in category_data: - # Use explicit display_name from YAML if provided, - # otherwise auto-generate from category_name - display_name = category_data.get( - "display_name", - category_data["category_name"].replace("_", " ").title(), + # Use explicit display_name if provided, otherwise auto-generate from category_name + display_name = category_data.get("display_name") or ( + category_data["category_name"].replace("_", " ").title() ) available_categories.append( diff --git a/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx b/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx index 0aad42feb08..fbd0af9918b 100644 --- a/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/add_guardrail_form.tsx @@ -1,4 +1,5 @@ -import { Button, Form, Input, Modal, Select, Steps, Tag, Typography } from "antd"; +import { Form, Input, Modal, Select, Tag, Typography } from "antd"; +import { Button } from "@tremor/react"; import React, { useEffect, useMemo, useState } from "react"; import NotificationsManager from "../molecules/notifications_manager"; import { createGuardrailCall, getGuardrailProviderSpecificParams, getGuardrailUISettings } from "../networking"; @@ -21,7 +22,6 @@ import ToolPermissionRulesEditor, { const { Title, Text, Link } = Typography; const { Option } = Select; -const { Step } = Steps; // Define human-friendly descriptions for each mode const modeDescriptions = { @@ -368,7 +368,7 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a // Validate that at least one content filter setting is configured if (selectedPatterns.length === 0 && blockedWords.length === 0 && selectedContentCategories.length === 0) { NotificationsManager.fromBackend( - "Please configure at least one content filter setting (category, pattern, or keyword)" + "Please configure at least one setting (denied topic, pattern, or word filter)" ); setLoading(false); return; @@ -769,83 +769,156 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a } }; - const renderStepButtons = () => { - const totalSteps = shouldRenderContentFilterConfigSettings(selectedProvider) ? 4 : 2; - const isLastStep = currentStep === totalSteps - 1; - const isCategoriesStep = shouldRenderContentFilterConfigSettings(selectedProvider) && currentStep === 1; - const hasPendingCategory = pendingCategorySelection !== ""; + const getStepConfigs = () => { + const isContentFilter = shouldRenderContentFilterConfigSettings(selectedProvider); + const isPII = shouldRenderPIIConfigSettings(selectedProvider); - return ( -
- {currentStep > 0 && ( - - )} - {isCategoriesStep ? ( - <> - - - - ) : ( - <> - {!isLastStep && ( - - )} - {isLastStep && ( - - )} - - )} - -
- ); + const steps = [ + { title: "Guardrail details", optional: false }, + { + title: isPII + ? "PII Configuration" + : isContentFilter + ? "Denied topics" + : "Provider Configuration", + optional: true, + }, + ]; + + if (isContentFilter) { + steps.push({ title: "Patterns", optional: true }); + steps.push({ title: "Word filters", optional: true }); + } + + return steps; }; - return ( - -
- - - - {shouldRenderContentFilterConfigSettings(selectedProvider) && ( - <> - - - - )} - + const stepConfigs = getStepConfigs(); - {renderStepContent()} - {renderStepButtons()} -
+ return ( + +
+ {/* Header */} +
+

Create guardrail

+ +
+ + {/* Scrollable content - inline vertical stepper */} +
+
+ {stepConfigs.map((step, index) => { + const isDone = index < currentStep; + const isCurrent = index === currentStep; + const isLast = index === stepConfigs.length - 1; + return ( +
+ {/* Vertical line + step indicator */} +
+
+ {isDone ? "\u2713" : index + 1} +
+ {!isLast && ( +
+ )} +
+ + {/* Step content */} +
+ {/* Step header - clickable for completed steps */} +
{ if (isDone) setCurrentStep(index); }} + style={{ minHeight: 24 }} + > + + {step.title} + + {step.optional && !isCurrent && ( + optional + )} + {isDone && ( + Edit + )} +
+ + {/* Expanded form content for current step */} + {isCurrent && ( +
+ {renderStepContent()} +
+ )} +
+
+ ); + })} + +
+ + {/* Bottom bar */} +
+ + {currentStep > 0 && ( + + )} + {currentStep < stepConfigs.length - 1 ? ( + + ) : ( + + )} +
+
); }; diff --git a/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentCategoryConfiguration.tsx b/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentCategoryConfiguration.tsx index 5ac5c70cd36..6408d676587 100644 --- a/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentCategoryConfiguration.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentCategoryConfiguration.tsx @@ -241,12 +241,12 @@ const ContentCategoryConfiguration: React.FC return ( +
- Content Categories + Blocked topics - - Detect harmful content, bias, and inappropriate advice using semantic analysis + + Select topics to block using keyword and semantic analysis
} @@ -316,10 +316,13 @@ const ContentCategoryConfiguration: React.FC borderRadius: "4px", overflow: "auto", maxHeight: "300px", + maxWidth: "100%", fontSize: "12px", lineHeight: "1.5", margin: 0, border: "1px solid #e0e0e0", + whiteSpace: "pre-wrap", + wordBreak: "break-word", }} > {previewYaml} @@ -410,7 +413,7 @@ const ContentCategoryConfiguration: React.FC borderRadius: "4px", }} > - No content categories selected. Add categories to detect harmful content, bias, or inappropriate advice. + No blocked topics selected. Add topics to detect and block harmful content.
)}