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.
This commit is contained in:
Ishaan Jaff 2026-02-20 18:42:11 -08:00 • committed by GitHub
parent e0129710c8
commit c61dea5af9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 160 additions and 86 deletions

View file

@ -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(

View file

@ -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<AddGuardrailFormProps> = ({ 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<AddGuardrailFormProps> = ({ 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 (
<div className="flex justify-end space-x-2 mt-4">
{currentStep > 0 && (
<Button onClick={prevStep}>
Previous
</Button>
)}
{isCategoriesStep ? (
<>
<Button onClick={nextStep}>
Skip
</Button>
<Button
type="primary"
onClick={handleAddAndContinue}
disabled={!hasPendingCategory}
>
Add & Continue →
</Button>
</>
) : (
<>
{!isLastStep && (
<Button type="primary" onClick={nextStep}>Next</Button>
)}
{isLastStep && (
<Button type="primary" onClick={handleSubmit} loading={loading}>
Create Guardrail
</Button>
)}
</>
)}
<Button onClick={handleClose}>
Cancel
</Button>
</div>
);
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 (
<Modal title="Add Guardrail" open={visible} onCancel={handleClose} footer={null} width={800}>
<Form
form={form}
layout="vertical"
initialValues={{
mode: "pre_call",
default_on: false,
}}
>
<Steps current={currentStep} className="mb-6" style={{ overflow: "visible" }}>
<Step title="Basic Info" />
<Step
title={
shouldRenderPIIConfigSettings(selectedProvider)
? "PII Configuration"
: shouldRenderContentFilterConfigSettings(selectedProvider)
? "Default Categories"
: "Provider Configuration"
}
/>
{shouldRenderContentFilterConfigSettings(selectedProvider) && (
<>
<Step title="Patterns" />
<Step title="Keywords" />
</>
)}
</Steps>
const stepConfigs = getStepConfigs();
{renderStepContent()}
{renderStepButtons()}
</Form>
return (
<Modal
title={null}
open={visible}
onCancel={handleClose}
footer={null}
width={1000}
closable={false}
className="top-8"
styles={{
body: { padding: 0 },
}}
>
<div className="flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h3 className="text-base font-semibold text-gray-900 m-0">Create guardrail</h3>
<button
onClick={handleClose}
className="text-gray-400 hover:text-gray-600 bg-transparent border-none cursor-pointer text-base leading-none p-1"
>
&#x2715;
</button>
</div>
{/* Scrollable content - inline vertical stepper */}
<div className="overflow-auto px-6 py-4" style={{ maxHeight: "calc(80vh - 120px)" }}>
<Form
form={form}
layout="vertical"
initialValues={{
mode: "pre_call",
default_on: false,
}}
>
{stepConfigs.map((step, index) => {
const isDone = index < currentStep;
const isCurrent = index === currentStep;
const isLast = index === stepConfigs.length - 1;
return (
<div key={index} className="relative flex gap-4" style={{ paddingBottom: isLast ? 0 : 8 }}>
{/* Vertical line + step indicator */}
<div className="flex flex-col items-center flex-shrink-0" style={{ width: 24 }}>
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium flex-shrink-0"
style={{
background: isDone ? "#4f46e5" : isCurrent ? "#fff" : "#f8fafc",
color: isDone ? "#fff" : isCurrent ? "#4f46e5" : "#94a3b8",
border: isCurrent ? "2px solid #4f46e5" : isDone ? "none" : "1px solid #e2e8f0",
}}
>
{isDone ? "\u2713" : index + 1}
</div>
{!isLast && (
<div
className="flex-1"
style={{
width: 1,
background: isDone ? "#4f46e5" : "#e2e8f0",
minHeight: 16,
}}
/>
)}
</div>
{/* Step content */}
<div className="flex-1 min-w-0" style={{ paddingBottom: isLast ? 0 : 16 }}>
{/* Step header - clickable for completed steps */}
<div
className={`flex items-center gap-2 ${isDone ? "cursor-pointer" : ""}`}
onClick={() => { if (isDone) setCurrentStep(index); }}
style={{ minHeight: 24 }}
>
<span
className="text-sm"
style={{
fontWeight: isCurrent ? 600 : 500,
color: isCurrent ? "#1e293b" : isDone ? "#4f46e5" : "#94a3b8",
}}
>
{step.title}
</span>
{step.optional && !isCurrent && (
<span className="text-[11px] text-slate-400">optional</span>
)}
{isDone && (
<span className="text-[11px] text-indigo-500 hover:underline">Edit</span>
)}
</div>
{/* Expanded form content for current step */}
{isCurrent && (
<div className="mt-3">
{renderStepContent()}
</div>
)}
</div>
</div>
);
})}
</Form>
</div>
{/* Bottom bar */}
<div className="flex items-center justify-end space-x-3 px-6 py-3 border-t border-gray-200">
<Button variant="secondary" onClick={handleClose}>
Cancel
</Button>
{currentStep > 0 && (
<Button variant="secondary" onClick={prevStep}>
Previous
</Button>
)}
{currentStep < stepConfigs.length - 1 ? (
<Button variant="primary" onClick={nextStep}>
Next
</Button>
) : (
<Button variant="primary" onClick={handleSubmit} loading={loading}>
Create Guardrail
</Button>
)}
</div>
</div>
</Modal>
);
};

View file

@ -241,12 +241,12 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
return (
<Card
title={
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 8 }}>
<Title level={5} style={{ margin: 0 }}>
Content Categories
Blocked topics
</Title>
<Text type="secondary" style={{ fontSize: 14, fontWeight: 400 }}>
Detect harmful content, bias, and inappropriate advice using semantic analysis
<Text type="secondary" style={{ fontSize: 12, fontWeight: 400 }}>
Select topics to block using keyword and semantic analysis
</Text>
</div>
}
@ -316,10 +316,13 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
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",
}}
>
<code>{previewYaml}</code>
@ -410,7 +413,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
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.
</div>
)}
</Card>