Fixing content filter guardrail update

This commit is contained in:
yuneng-jiang
2026-02-11 15:37:57 -08:00
parent 5fa5657476
commit 83f92d6ba5
16 changed files with 761 additions and 260 deletions
@@ -200,6 +200,7 @@ export const ModelSelect = (props: ModelSelectProps) => {
},
]}
mode="multiple"
placeholder="Select Models"
allowClear
maxTagCount="responsive"
maxTagPlaceholder={(omittedValues) => (
@@ -6,6 +6,7 @@ import UsageIndicator from "./UsageIndicator";
vi.mock("./networking", () => ({
getRemainingUsers: vi.fn(),
getLicenseInfo: vi.fn().mockResolvedValue(null),
}));
vi.mock("@/app/(dashboard)/hooks/useDisableUsageIndicator", () => ({
@@ -1,21 +1,20 @@
import React, { useState, useEffect, useMemo } from "react";
import { Form, Typography, Select, Modal, Tag, Steps } from "antd";
import { Button, TextInput } from "@tremor/react";
import {
guardrail_provider_map,
shouldRenderPIIConfigSettings,
shouldRenderContentFilterConfigSettings,
guardrailLogoMap,
populateGuardrailProviders,
populateGuardrailProviderMap,
getGuardrailProviders,
} from "./guardrail_info_helpers";
import { createGuardrailCall, getGuardrailUISettings, getGuardrailProviderSpecificParams } from "../networking";
import PiiConfiguration from "./pii_configuration";
import GuardrailProviderFields from "./guardrail_provider_fields";
import GuardrailOptionalParams from "./guardrail_optional_params";
import { Button, Form, Input, Modal, Select, Steps, Tag, Typography } from "antd";
import React, { useEffect, useMemo, useState } from "react";
import NotificationsManager from "../molecules/notifications_manager";
import { createGuardrailCall, getGuardrailProviderSpecificParams, getGuardrailUISettings } from "../networking";
import ContentFilterConfiguration from "./content_filter/ContentFilterConfiguration";
import {
getGuardrailProviders,
guardrail_provider_map,
guardrailLogoMap,
populateGuardrailProviderMap,
populateGuardrailProviders,
shouldRenderContentFilterConfigSettings,
shouldRenderPIIConfigSettings,
} from "./guardrail_info_helpers";
import GuardrailOptionalParams from "./guardrail_optional_params";
import GuardrailProviderFields from "./guardrail_provider_fields";
import PiiConfiguration from "./pii_configuration";
import ToolPermissionRulesEditor, {
ToolPermissionConfig,
} from "./tool_permission/ToolPermissionRulesEditor";
@@ -457,7 +456,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
label="Guardrail Name"
rules={[{ required: true, message: "Please enter a guardrail name" }]}
>
<TextInput placeholder="Enter a name for this guardrail" />
<Input placeholder="Enter a name for this guardrail" />
</Form.Item>
<Form.Item
@@ -547,41 +546,41 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
</div>
</Option>
)) || (
<>
<Option value="pre_call" label="pre_call">
<div>
<>
<Option value="pre_call" label="pre_call">
<div>
<strong>pre_call</strong> <Tag color="green">Recommended</Tag>
<div>
<strong>pre_call</strong> <Tag color="green">Recommended</Tag>
</div>
<div style={{ fontSize: "12px", color: "#888" }}>{modeDescriptions.pre_call}</div>
</div>
<div style={{ fontSize: "12px", color: "#888" }}>{modeDescriptions.pre_call}</div>
</div>
</Option>
<Option value="during_call" label="during_call">
<div>
</Option>
<Option value="during_call" label="during_call">
<div>
<strong>during_call</strong>
<div>
<strong>during_call</strong>
</div>
<div style={{ fontSize: "12px", color: "#888" }}>{modeDescriptions.during_call}</div>
</div>
<div style={{ fontSize: "12px", color: "#888" }}>{modeDescriptions.during_call}</div>
</div>
</Option>
<Option value="post_call" label="post_call">
<div>
</Option>
<Option value="post_call" label="post_call">
<div>
<strong>post_call</strong>
<div>
<strong>post_call</strong>
</div>
<div style={{ fontSize: "12px", color: "#888" }}>{modeDescriptions.post_call}</div>
</div>
<div style={{ fontSize: "12px", color: "#888" }}>{modeDescriptions.post_call}</div>
</div>
</Option>
<Option value="logging_only" label="logging_only">
<div>
</Option>
<Option value="logging_only" label="logging_only">
<div>
<strong>logging_only</strong>
<div>
<strong>logging_only</strong>
</div>
<div style={{ fontSize: "12px", color: "#888" }}>{modeDescriptions.logging_only}</div>
</div>
<div style={{ fontSize: "12px", color: "#888" }}>{modeDescriptions.logging_only}</div>
</div>
</Option>
</>
)}
</Option>
</>
)}
</Select>
</Form.Item>
@@ -721,21 +720,21 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
const renderStepButtons = () => {
const totalSteps = shouldRenderContentFilterConfigSettings(selectedProvider) ? 4 : 2;
const isLastStep = currentStep === totalSteps - 1;
return (
<div className="flex justify-end space-x-2 mt-4">
{currentStep > 0 && (
<Button variant="secondary" onClick={prevStep}>
<Button onClick={prevStep}>
Previous
</Button>
)}
{!isLastStep && <Button onClick={nextStep}>Next</Button>}
{!isLastStep && <Button type="primary" onClick={nextStep}>Next</Button>}
{isLastStep && (
<Button onClick={handleSubmit} loading={loading}>
<Button type="primary" onClick={handleSubmit} loading={loading}>
Create Guardrail
</Button>
)}
<Button variant="secondary" onClick={handleClose}>
<Button onClick={handleClose}>
Cancel
</Button>
</div>
@@ -1,12 +1,10 @@
import React from "react";
import { Card, Typography, Select, Table, Tag, Collapse } from "antd";
import { Card, Typography, Select, Table, Tag, Collapse, Button } from "antd";
import { DeleteOutlined, PlusOutlined, FileTextOutlined } from "@ant-design/icons";
import { Button } from "@tremor/react";
import { getCategoryYaml } from "../../networking";
const { Title, Text } = Typography;
const { Option } = Select;
const { Panel } = Collapse;
interface ContentCategory {
name: string;
@@ -191,10 +189,9 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
width: 80,
render: (_: any, record: SelectedCategory) => (
<Button
icon={DeleteOutlined}
icon={<DeleteOutlined />}
onClick={() => onCategoryRemove(record.id)}
variant="secondary"
size="xs"
size="small"
>
Remove
</Button>
@@ -244,9 +241,10 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
))}
</Select>
<Button
type="primary"
onClick={handleAddCategory}
disabled={!selectedCategoryName}
icon={PlusOutlined}
icon={<PlusOutlined />}
>
Add
</Button>
@@ -308,7 +306,6 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
activeKey={expandedYamlCategories}
onChange={(keys) => {
const keyArray = Array.isArray(keys) ? keys : keys ? [keys] : [];
const newExpanded = new Set(keyArray as string[]);
const oldExpanded = new Set(expandedYamlCategories);
// Find newly expanded categories and fetch their YAML
@@ -322,44 +319,40 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
setExpandedYamlCategories(keyArray as string[]);
}}
ghost
>
{selectedCategories.map((category) => (
<Panel
header={
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<FileTextOutlined />
<span>View YAML for {category.display_name}</span>
</div>
}
key={category.category}
>
{loadingYaml[category.category] ? (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
Loading YAML...
</div>
) : categoryYaml[category.category] ? (
<pre
style={{
background: "#f5f5f5",
padding: "16px",
borderRadius: "4px",
overflow: "auto",
maxHeight: "400px",
fontSize: "12px",
lineHeight: "1.5",
margin: 0,
}}
>
<code>{categoryYaml[category.category]}</code>
</pre>
) : (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
YAML will load when expanded
</div>
)}
</Panel>
))}
</Collapse>
items={selectedCategories.map((category) => ({
key: category.category,
label: (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<FileTextOutlined />
<span>View YAML for {category.display_name}</span>
</div>
),
children: loadingYaml[category.category] ? (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
Loading YAML...
</div>
) : categoryYaml[category.category] ? (
<pre
style={{
background: "#f5f5f5",
padding: "16px",
borderRadius: "4px",
overflow: "auto",
maxHeight: "400px",
fontSize: "12px",
lineHeight: "1.5",
margin: 0,
}}
>
<code>{categoryYaml[category.category]}</code>
</pre>
) : (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
YAML will load when expanded
</div>
),
}))}
/>
</div>
</>
) : (
@@ -1,7 +1,6 @@
import React, { useState } from "react";
import { Typography, Space, Upload, Card } from "antd";
import { Typography, Space, Upload, Card, Button } from "antd";
import { PlusOutlined, UploadOutlined } from "@ant-design/icons";
import { Button } from "@tremor/react";
import { validateBlockedWordsFile } from "../../networking";
import NotificationsManager from "../../molecules/notifications_manager";
import PatternModal from "./PatternModal";
@@ -221,10 +220,10 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
>
<div style={{ marginBottom: 16 }}>
<Space>
<Button type="button" onClick={() => setPatternModalVisible(true)} icon={PlusOutlined}>
<Button type="primary" onClick={() => setPatternModalVisible(true)} icon={<PlusOutlined />}>
Add prebuilt pattern
</Button>
<Button type="button" onClick={() => setCustomPatternModalVisible(true)} variant="secondary" icon={PlusOutlined}>
<Button onClick={() => setCustomPatternModalVisible(true)} icon={<PlusOutlined />}>
Add custom regex
</Button>
</Space>
@@ -253,11 +252,11 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
>
<div style={{ marginBottom: 16 }}>
<Space>
<Button type="button" onClick={() => setKeywordModalVisible(true)} icon={PlusOutlined}>
<Button type="primary" onClick={() => setKeywordModalVisible(true)} icon={<PlusOutlined />}>
Add keyword
</Button>
<Upload beforeUpload={handleFileUpload} accept=".yaml,.yml" showUploadList={false}>
<Button type="button" variant="secondary" icon={UploadOutlined} loading={uploadValidating}>
<Button icon={<UploadOutlined />} loading={uploadValidating}>
Upload YAML file
</Button>
</Upload>
@@ -1,58 +1,195 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import ContentFilterManager from "./ContentFilterManager";
import userEvent from "@testing-library/user-event";
import ContentFilterManager, {
formatContentFilterDataForAPI,
} from "./ContentFilterManager";
import React from "react";
const CONTENT_FILTER_GUARDRAIL_DATA = {
litellm_params: {
guardrail: "litellm_content_filter",
patterns: [{ pattern_type: "prebuilt", pattern_name: "email", action: "BLOCK" }],
blocked_words: [{ keyword: "test", action: "BLOCK", description: null }],
},
};
const GUARDRAIL_SETTINGS = {
content_filter_settings: {
prebuilt_patterns: [],
pattern_categories: ["PII"],
supported_actions: ["BLOCK", "MASK"],
},
};
vi.mock("./ContentFilterConfiguration", () => ({
default: () => <div data-testid="content-filter-config">Mock Content Filter Configuration</div>
default: ({
onPatternAdd,
onPatternRemove,
onBlockedWordAdd,
onBlockedWordRemove,
selectedPatterns,
blockedWords,
}: {
onPatternAdd: (p: object) => void;
onPatternRemove: (id: string) => void;
onBlockedWordAdd: (w: object) => void;
onBlockedWordRemove: (id: string) => void;
selectedPatterns: { id: string }[];
blockedWords: { id: string }[];
}) => (
<div data-testid="content-filter-config">
<button
type="button"
onClick={() =>
onPatternAdd({
id: "pattern-new",
type: "prebuilt",
name: "ssn",
action: "BLOCK",
})
}
>
Add pattern
</button>
<button
type="button"
onClick={() =>
onBlockedWordAdd({
id: "word-new",
keyword: "secret",
action: "MASK",
})
}
>
Add keyword
</button>
{selectedPatterns[0] && (
<button
type="button"
onClick={() => onPatternRemove(selectedPatterns[0].id)}
>
Remove pattern
</button>
)}
{blockedWords[0] && (
<button
type="button"
onClick={() => onBlockedWordRemove(blockedWords[0].id)}
>
Remove keyword
</button>
)}
</div>
),
}));
vi.mock("./ContentFilterDisplay", () => ({
default: () => <div data-testid="content-filter-display">Mock Content Filter Display</div>
default: ({
patterns,
blockedWords,
}: {
patterns: { name: string }[];
blockedWords: { keyword: string }[];
}) => (
<div data-testid="content-filter-display">
<span>Patterns: {patterns.map((p) => p.name).join(", ")}</span>
<span>Keywords: {blockedWords.map((w) => w.keyword).join(", ")}</span>
</div>
),
}));
vi.mock("antd", () => ({
Divider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>
}));
vi.mock("antd", async (importOriginal) => {
const actual = await importOriginal<typeof import("antd")>();
return {
...actual,
Divider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="divider">{children}</div>
),
Alert: ({
message,
type,
}: {
message: React.ReactNode;
type: string;
}) => (
<div data-testid="unsaved-alert" data-type={type}>
{message}
</div>
),
};
});
describe("ContentFilterManager - Unsaved Changes Detection", () => {
describe("ContentFilterManager", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should render when guardrail is content filter and isEditing is true", async () => {
render(
<ContentFilterManager
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={true}
accessToken="test-token"
/>
);
await waitFor(() => {
expect(screen.getByTestId("content-filter-config")).toBeInTheDocument();
});
expect(screen.getByTestId("divider")).toHaveTextContent(
"Content Filter Configuration"
);
});
it("should return null when guardrail is not litellm_content_filter", () => {
const guardrailData = {
litellm_params: { guardrail: "presidio" },
};
const { container } = render(
<ContentFilterManager
guardrailData={guardrailData}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={true}
accessToken="test-token"
/>
);
expect(screen.queryByTestId("content-filter-config")).not.toBeInTheDocument();
expect(screen.queryByTestId("content-filter-display")).not.toBeInTheDocument();
expect(container.firstChild).toBeNull();
});
it("should render read-only display when isEditing is false", async () => {
render(
<ContentFilterManager
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={false}
accessToken="test-token"
/>
);
await waitFor(() => {
expect(screen.getByTestId("content-filter-display")).toBeInTheDocument();
});
expect(screen.queryByTestId("content-filter-config")).not.toBeInTheDocument();
expect(screen.getByText(/email/)).toBeInTheDocument();
expect(screen.getByText(/test/)).toBeInTheDocument();
});
it("should call onUnsavedChanges with false when component initializes with matching data", async () => {
/**
* Tests that the ContentFilterManager correctly initializes the unsaved changes
* detection and calls onUnsavedChanges(false) when the current state matches
* the original loaded state (no changes yet).
*/
const mockOnUnsavedChanges = vi.fn();
const mockOnDataChange = vi.fn();
const guardrailData = {
litellm_params: {
guardrail: "litellm_content_filter",
patterns: [
{ pattern_type: "prebuilt", pattern_name: "email", action: "BLOCK" }
],
blocked_words: [
{ keyword: "test", action: "BLOCK", description: null }
]
}
};
const guardrailSettings = {
content_filter_settings: {
prebuilt_patterns: [],
pattern_categories: ["PII"],
supported_actions: ["BLOCK", "MASK"]
}
};
render(
<ContentFilterManager
guardrailData={guardrailData}
guardrailSettings={guardrailSettings}
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={true}
accessToken="test-token"
onDataChange={mockOnDataChange}
@@ -60,18 +197,312 @@ describe("ContentFilterManager - Unsaved Changes Detection", () => {
/>
);
// Wait for component to render in edit mode
await waitFor(() => {
expect(screen.getByTestId("content-filter-config")).toBeInTheDocument();
});
// Verify onUnsavedChanges was called with false (no changes initially)
await waitFor(() => {
expect(mockOnUnsavedChanges).toHaveBeenCalledWith(false);
});
// Verify onDataChange was called with initial data
expect(mockOnDataChange).toHaveBeenCalled();
});
it("should call onUnsavedChanges with true when user adds a pattern", async () => {
const mockOnUnsavedChanges = vi.fn();
const user = userEvent.setup();
render(
<ContentFilterManager
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={true}
accessToken="test-token"
onUnsavedChanges={mockOnUnsavedChanges}
/>
);
await waitFor(() => {
expect(screen.getByTestId("content-filter-config")).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: /add pattern/i }));
await waitFor(() => {
expect(mockOnUnsavedChanges).toHaveBeenCalledWith(true);
});
});
it("should call onUnsavedChanges with true when user adds a keyword", async () => {
const mockOnUnsavedChanges = vi.fn();
const user = userEvent.setup();
render(
<ContentFilterManager
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={true}
accessToken="test-token"
onUnsavedChanges={mockOnUnsavedChanges}
/>
);
await waitFor(() => {
expect(screen.getByTestId("content-filter-config")).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: /add keyword/i }));
await waitFor(() => {
expect(mockOnUnsavedChanges).toHaveBeenCalledWith(true);
});
});
it("should call onUnsavedChanges with true when user removes a pattern", async () => {
const mockOnUnsavedChanges = vi.fn();
const user = userEvent.setup();
render(
<ContentFilterManager
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={true}
accessToken="test-token"
onUnsavedChanges={mockOnUnsavedChanges}
/>
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /remove pattern/i })).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: /remove pattern/i }));
await waitFor(() => {
expect(mockOnUnsavedChanges).toHaveBeenCalledWith(true);
});
});
it("should show unsaved changes alert when data has changed", async () => {
const user = userEvent.setup();
render(
<ContentFilterManager
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={true}
accessToken="test-token"
/>
);
await waitFor(() => {
expect(screen.getByTestId("content-filter-config")).toBeInTheDocument();
});
expect(screen.queryByTestId("unsaved-alert")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /add pattern/i }));
await waitFor(() => {
expect(screen.getByTestId("unsaved-alert")).toBeInTheDocument();
});
expect(screen.getByTestId("unsaved-alert")).toHaveTextContent(
/unsaved changes.*Save Changes/i
);
});
it("should call onDataChange when patterns or keywords change", async () => {
const mockOnDataChange = vi.fn();
const user = userEvent.setup();
render(
<ContentFilterManager
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={true}
accessToken="test-token"
onDataChange={mockOnDataChange}
/>
);
await waitFor(() => {
expect(mockOnDataChange).toHaveBeenCalled();
});
const initialCalls = mockOnDataChange.mock.calls.length;
await user.click(screen.getByRole("button", { name: /add keyword/i }));
await waitFor(() => {
expect(mockOnDataChange.mock.calls.length).toBeGreaterThan(initialCalls);
});
const lastCall = mockOnDataChange.mock.calls[mockOnDataChange.mock.calls.length - 1];
const blockedWords = lastCall[1];
expect(blockedWords).toContainEqual(
expect.objectContaining({ keyword: "secret", action: "MASK" })
);
});
it("should not call onUnsavedChanges when isEditing is false", async () => {
const mockOnUnsavedChanges = vi.fn();
render(
<ContentFilterManager
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={false}
accessToken="test-token"
onUnsavedChanges={mockOnUnsavedChanges}
/>
);
await waitFor(() => {
expect(screen.getByTestId("content-filter-display")).toBeInTheDocument();
});
expect(mockOnUnsavedChanges).not.toHaveBeenCalled();
});
it("should initialize with empty data when guardrailData has no patterns or blocked_words", async () => {
const guardrailData = {
litellm_params: {
guardrail: "litellm_content_filter",
},
};
const mockOnDataChange = vi.fn();
render(
<ContentFilterManager
guardrailData={guardrailData}
guardrailSettings={GUARDRAIL_SETTINGS}
isEditing={true}
accessToken="test-token"
onDataChange={mockOnDataChange}
/>
);
await waitFor(() => {
expect(mockOnDataChange).toHaveBeenCalledWith([], [], []);
});
});
it("should not render ContentFilterConfiguration when guardrailSettings has no content_filter_settings", async () => {
render(
<ContentFilterManager
guardrailData={CONTENT_FILTER_GUARDRAIL_DATA}
guardrailSettings={null}
isEditing={true}
accessToken="test-token"
/>
);
await waitFor(() => {
expect(screen.getByTestId("divider")).toBeInTheDocument();
});
expect(screen.queryByTestId("content-filter-config")).not.toBeInTheDocument();
});
});
describe("formatContentFilterDataForAPI", () => {
it("should format patterns and blocked words for API", () => {
const patterns = [
{
id: "p1",
type: "prebuilt" as const,
name: "email",
action: "BLOCK" as const,
},
{
id: "p2",
type: "custom" as const,
name: "custom",
pattern: "\\d+",
action: "MASK" as const,
},
];
const blockedWords = [
{
id: "w1",
keyword: "secret",
action: "MASK" as const,
description: "-sensitive",
},
];
const result = formatContentFilterDataForAPI(patterns, blockedWords);
expect(result.patterns).toEqual([
{
pattern_type: "prebuilt",
pattern_name: "email",
pattern: undefined,
name: "email",
action: "BLOCK",
},
{
pattern_type: "regex",
pattern_name: undefined,
pattern: "\\d+",
name: "custom",
action: "MASK",
},
]);
expect(result.blocked_words).toEqual([
{ keyword: "secret", action: "MASK", description: "-sensitive" },
]);
expect(result.categories).toBeUndefined();
});
it("should include categories when provided", () => {
const patterns: Parameters<typeof formatContentFilterDataForAPI>[0] = [];
const blockedWords: Parameters<typeof formatContentFilterDataForAPI>[1] = [];
const categories = [
{
id: "c1",
category: "PII",
display_name: "PII",
action: "BLOCK" as const,
severity_threshold: "high" as const,
},
];
const result = formatContentFilterDataForAPI(patterns, blockedWords, categories);
expect(result.categories).toEqual([
{
category: "PII",
enabled: true,
action: "BLOCK",
severity_threshold: "high",
},
]);
});
it("should use medium as default severity_threshold when category has none", () => {
const patterns: Parameters<typeof formatContentFilterDataForAPI>[0] = [];
const blockedWords: Parameters<typeof formatContentFilterDataForAPI>[1] = [];
const categories = [
{
id: "c1",
category: "PII",
display_name: "PII",
action: "MASK" as const,
severity_threshold: undefined as unknown as "high" | "medium" | "low",
},
];
const result = formatContentFilterDataForAPI(patterns, blockedWords, categories);
expect(result.categories).toEqual([
{
category: "PII",
enabled: true,
action: "MASK",
severity_threshold: "medium",
},
]);
});
});
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from "react";
import { Divider } from "antd";
import { Alert, Divider, Typography } from "antd";
import React, { useEffect, useState } from "react";
import ContentFilterConfiguration from "./ContentFilterConfiguration";
import ContentFilterDisplay from "./ContentFilterDisplay";
const { Text } = Typography
interface Pattern {
id: string;
type: "prebuilt" | "custom";
@@ -19,6 +21,21 @@ interface BlockedWord {
description?: string;
}
interface SelectedContentCategory {
id: string;
category: string;
display_name: string;
action: "BLOCK" | "MASK";
severity_threshold: "high" | "medium" | "low";
}
interface ContentCategory {
name: string;
display_name: string;
description: string;
default_action: string;
}
interface GuardrailSettings {
content_filter_settings?: {
prebuilt_patterns: Array<{
@@ -29,6 +46,7 @@ interface GuardrailSettings {
}>;
pattern_categories: string[];
supported_actions: string[];
content_categories?: ContentCategory[];
};
}
@@ -37,7 +55,7 @@ interface ContentFilterManagerProps {
guardrailSettings: GuardrailSettings | null;
isEditing: boolean;
accessToken: string | null;
onDataChange?: (patterns: Pattern[], blockedWords: BlockedWord[]) => void;
onDataChange?: (patterns: Pattern[], blockedWords: BlockedWord[], categories: SelectedContentCategory[]) => void;
onUnsavedChanges?: (hasChanges: boolean) => void;
}
@@ -51,8 +69,10 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
}) => {
const [selectedPatterns, setSelectedPatterns] = useState<Pattern[]>([]);
const [blockedWords, setBlockedWords] = useState<BlockedWord[]>([]);
const [selectedContentCategories, setSelectedContentCategories] = useState<SelectedContentCategory[]>([]);
const [originalPatterns, setOriginalPatterns] = useState<Pattern[]>([]);
const [originalBlockedWords, setOriginalBlockedWords] = useState<BlockedWord[]>([]);
const [originalContentCategories, setOriginalContentCategories] = useState<SelectedContentCategory[]>([]);
// Load data from guardrail on mount or when guardrailData changes
useEffect(() => {
@@ -85,21 +105,45 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
setBlockedWords([]);
setOriginalBlockedWords([]);
}
}, [guardrailData]);
if (guardrailData?.litellm_params?.categories?.length > 0) {
const contentCategoriesMap = guardrailSettings?.content_filter_settings?.content_categories
? Object.fromEntries(
guardrailSettings.content_filter_settings.content_categories.map((c) => [c.name, c])
)
: {};
const categories = guardrailData.litellm_params.categories.map((c: any, index: number) => {
const meta = contentCategoriesMap[c.category];
return {
id: `category-${index}`,
category: c.category,
display_name: meta?.display_name ?? c.category,
action: (c.action || "BLOCK") as "BLOCK" | "MASK",
severity_threshold: (c.severity_threshold || "medium") as "high" | "medium" | "low",
};
});
setSelectedContentCategories(categories);
setOriginalContentCategories(categories);
} else {
setSelectedContentCategories([]);
setOriginalContentCategories([]);
}
}, [guardrailData, guardrailSettings?.content_filter_settings?.content_categories]);
// Notify parent component when data changes
useEffect(() => {
if (onDataChange) {
onDataChange(selectedPatterns, blockedWords);
onDataChange(selectedPatterns, blockedWords, selectedContentCategories);
}
}, [selectedPatterns, blockedWords, onDataChange]);
}, [selectedPatterns, blockedWords, selectedContentCategories, onDataChange]);
// Detect unsaved changes
const hasUnsavedChanges = React.useMemo(() => {
const hasPatternChanges = JSON.stringify(selectedPatterns) !== JSON.stringify(originalPatterns);
const hasWordChanges = JSON.stringify(blockedWords) !== JSON.stringify(originalBlockedWords);
return hasPatternChanges || hasWordChanges;
}, [selectedPatterns, blockedWords, originalPatterns, originalBlockedWords]);
const hasCategoryChanges = JSON.stringify(selectedContentCategories) !== JSON.stringify(originalContentCategories);
return hasPatternChanges || hasWordChanges || hasCategoryChanges;
}, [selectedPatterns, blockedWords, selectedContentCategories, originalPatterns, originalBlockedWords, originalContentCategories]);
useEffect(() => {
if (isEditing && onUnsavedChanges) {
@@ -122,12 +166,17 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
<>
<Divider orientation="left">Content Filter Configuration</Divider>
{hasUnsavedChanges && (
<div className="mb-4 px-4 py-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-sm text-yellow-800 font-medium">
You have unsaved changes to patterns or keywords. Remember to click &quot;Save Changes&quot; at the
bottom.
</p>
</div>
<Alert
type="warning"
showIcon
className="mb-4"
message={
<Text>
You have unsaved changes to patterns or keywords. Remember to click &quot;Save Changes&quot; at the
bottom.
</Text>
}
/>
)}
<div className="mb-6">
{guardrailSettings && guardrailSettings.content_filter_settings && (
@@ -150,6 +199,19 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
console.log("File uploaded:", content);
}}
accessToken={accessToken}
contentCategories={guardrailSettings.content_filter_settings.content_categories || []}
selectedContentCategories={selectedContentCategories}
onContentCategoryAdd={(category) =>
setSelectedContentCategories([...selectedContentCategories, category])
}
onContentCategoryRemove={(id) =>
setSelectedContentCategories(selectedContentCategories.filter((c) => c.id !== id))
}
onContentCategoryUpdate={(id, field, value) =>
setSelectedContentCategories(
selectedContentCategories.map((c) => (c.id === id ? { ...c, [field]: value } : c))
)
}
/>
)}
</div>
@@ -160,8 +222,16 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
export default ContentFilterManager;
// Helper function to format data for API
export const formatContentFilterDataForAPI = (patterns: Pattern[], blockedWords: BlockedWord[]) => {
return {
export const formatContentFilterDataForAPI = (
patterns: Pattern[],
blockedWords: BlockedWord[],
categories?: SelectedContentCategory[]
) => {
const result: {
patterns: any[];
blocked_words: any[];
categories?: any[];
} = {
patterns: patterns.map((p) => ({
pattern_type: p.type === "prebuilt" ? "prebuilt" : "regex",
pattern_name: p.type === "prebuilt" ? p.name : undefined,
@@ -175,4 +245,13 @@ export const formatContentFilterDataForAPI = (patterns: Pattern[], blockedWords:
description: w.description,
})),
};
if (categories !== undefined) {
result.categories = categories.map((c) => ({
category: c.category,
enabled: true,
action: c.action,
severity_threshold: c.severity_threshold || "medium",
}));
}
return result;
};
@@ -1,6 +1,5 @@
import React from "react";
import { Typography, Select, Modal, Space } from "antd";
import { Button, TextInput } from "@tremor/react";
import { Typography, Select, Modal, Space, Button, Input } from "antd";
const { Text } = Typography;
const { Option } = Select;
@@ -39,20 +38,20 @@ const CustomPatternModal: React.FC<CustomPatternModalProps> = ({
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div>
<Text strong>Pattern name</Text>
<TextInput
<Input
placeholder="e.g., internal_id, employee_code"
value={patternName}
onValueChange={onNameChange}
onChange={(e) => onNameChange(e.target.value)}
style={{ marginTop: 8 }}
/>
</div>
<div>
<Text strong>Regex pattern</Text>
<TextInput
<Input
placeholder="e.g., ID-[0-9]{6}"
value={patternRegex}
onValueChange={onRegexChange}
onChange={(e) => onRegexChange(e.target.value)}
style={{ marginTop: 8 }}
/>
<Text type="secondary" style={{ fontSize: 12 }}>
@@ -77,10 +76,10 @@ const CustomPatternModal: React.FC<CustomPatternModalProps> = ({
</Space>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "24px" }}>
<Button variant="secondary" onClick={onCancel}>
<Button onClick={onCancel}>
Cancel
</Button>
<Button onClick={onAdd}>
<Button type="primary" onClick={onAdd}>
Add
</Button>
</div>
@@ -1,6 +1,5 @@
import React from "react";
import { Typography, Select, Modal, Space } from "antd";
import { Button, TextInput, Textarea } from "@tremor/react";
import { Typography, Select, Modal, Space, Button, Input } from "antd";
const { Text } = Typography;
const { Option } = Select;
@@ -39,10 +38,10 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div>
<Text strong>Keyword</Text>
<TextInput
<Input
placeholder="Enter sensitive keyword or phrase"
value={keyword}
onValueChange={onKeywordChange}
onChange={(e) => onKeywordChange(e.target.value)}
style={{ marginTop: 8 }}
/>
</div>
@@ -64,10 +63,10 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
<div>
<Text strong>Description (optional)</Text>
<Textarea
<Input.TextArea
placeholder="Explain why this keyword is sensitive"
value={description}
onValueChange={onDescriptionChange}
onChange={(e) => onDescriptionChange(e.target.value)}
rows={3}
style={{ marginTop: 8 }}
/>
@@ -75,10 +74,10 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
</Space>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "24px" }}>
<Button variant="secondary" onClick={onCancel}>
<Button onClick={onCancel}>
Cancel
</Button>
<Button onClick={onAdd}>
<Button type="primary" onClick={onAdd}>
Add
</Button>
</div>
@@ -1,7 +1,6 @@
import React from "react";
import { Typography, Select, Table } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
import { Button } from "@tremor/react";
import { Button, Select, Table, Typography } from "antd";
import React from "react";
const { Text } = Typography;
const { Option } = Select;
@@ -59,11 +58,10 @@ const KeywordTable: React.FC<KeywordTableProps> = ({
width: 100,
render: (_: any, record: BlockedWord) => (
<Button
type="button"
variant="light"
color="red"
size="xs"
icon={DeleteOutlined}
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => onRemove(record.id)}
>
Delete
@@ -1,6 +1,5 @@
import React from "react";
import { Typography, Select, Modal, Space } from "antd";
import { Button } from "@tremor/react";
import { Typography, Select, Modal, Space, Button } from "antd";
const { Text } = Typography;
const { Option } = Select;
@@ -97,10 +96,10 @@ const PatternModal: React.FC<PatternModalProps> = ({
</Space>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "24px" }}>
<Button variant="secondary" onClick={onCancel}>
<Button onClick={onCancel}>
Cancel
</Button>
<Button onClick={onAdd}>
<Button type="primary" onClick={onAdd}>
Add
</Button>
</div>
@@ -1,7 +1,6 @@
import React from "react";
import { Typography, Select, Table, Tag } from "antd";
import { Typography, Select, Table, Tag, Button } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
import { Button } from "@tremor/react";
const { Text } = Typography;
const { Option } = Select;
@@ -73,11 +72,10 @@ const PatternTable: React.FC<PatternTableProps> = ({
width: 100,
render: (_: any, record: Pattern) => (
<Button
type="button"
variant="light"
color="red"
size="xs"
icon={DeleteOutlined}
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => onRemove(record.id)}
>
Delete
@@ -20,16 +20,17 @@ vi.mock("./content_filter/ContentFilterManager", () => ({
{isEditing && (
<button onClick={() => {
onUnsavedChanges(true);
onDataChange?.(["new_pattern"], ["new_word"]);
onDataChange?.(["new_pattern"], ["new_word"], []);
}}>
Simulate Change
</button>
)}
</div>
),
formatContentFilterDataForAPI: (patterns: any[], blockedWords: any[]) => ({
formatContentFilterDataForAPI: (patterns: any[], blockedWords: any[], categories?: any[]) => ({
patterns,
blocked_words: blockedWords,
categories: categories ?? [],
}),
}));
@@ -63,7 +64,7 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
const { getAllByText, getByText } = render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />,
<GuardrailInfoView guardrailId="123" onClose={() => { }} accessToken="123" isAdmin={true} />,
);
// Wait for the loading to complete and data to be rendered
@@ -104,7 +105,7 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
const { getByText, container } = render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />,
<GuardrailInfoView guardrailId="123" onClose={() => { }} accessToken="123" isAdmin={true} />,
);
await waitFor(() => {
@@ -162,7 +163,7 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
const { getByText } = render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />,
<GuardrailInfoView guardrailId="123" onClose={() => { }} accessToken="123" isAdmin={true} />,
);
await waitFor(() => {
@@ -1,39 +1,37 @@
import React, { useState, useEffect, useCallback } from "react";
import {
Card,
Title,
Text,
Grid,
getGuardrailInfo,
getGuardrailProviderSpecificParams,
getGuardrailUISettings,
updateGuardrailCall,
} from "@/components/networking";
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { CodeOutlined, EyeInvisibleOutlined, InfoCircleOutlined, StopOutlined } from "@ant-design/icons";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import {
Badge,
Button as TremorButton,
Card,
Grid,
Tab,
TabGroup,
TabList,
TabPanel,
TabPanels,
TextInput,
Text,
Title,
} from "@tremor/react";
import { Button, Form, Input, Select, Divider, Tooltip } from "antd";
import { InfoCircleOutlined, EyeInvisibleOutlined, StopOutlined, CodeOutlined } from "@ant-design/icons";
import {
getGuardrailInfo,
updateGuardrailCall,
getGuardrailUISettings,
getGuardrailProviderSpecificParams,
} from "@/components/networking";
import { getGuardrailLogoAndName, guardrail_provider_map } from "./guardrail_info_helpers";
import PiiConfiguration from "./pii_configuration";
import GuardrailProviderFields from "./guardrail_provider_fields";
import GuardrailOptionalParams from "./guardrail_optional_params";
import { Button, Divider, Form, Input, Select, Tooltip } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
import React, { useCallback, useEffect, useState } from "react";
import NotificationsManager from "../molecules/notifications_manager";
import ContentFilterManager, { formatContentFilterDataForAPI } from "./content_filter/ContentFilterManager";
import CustomCodeModal, { EditGuardrailData } from "./custom_code/CustomCodeModal";
import { getGuardrailLogoAndName, guardrail_provider_map } from "./guardrail_info_helpers";
import GuardrailOptionalParams from "./guardrail_optional_params";
import GuardrailProviderFields from "./guardrail_provider_fields";
import PiiConfiguration from "./pii_configuration";
import ToolPermissionRulesEditor, {
ToolPermissionConfig,
} from "./tool_permission/ToolPermissionRulesEditor";
import CustomCodeModal, { EditGuardrailData } from "./custom_code/CustomCodeModal";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { CheckIcon, CopyIcon } from "lucide-react";
import NotificationsManager from "../molecules/notifications_manager";
export interface GuardrailInfoProps {
guardrailId: string;
@@ -83,6 +81,12 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
}>;
pattern_categories: string[];
supported_actions: string[];
content_categories?: Array<{
name: string;
display_name: string;
description: string;
default_action: string;
}>;
};
} | null>(null);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
@@ -98,14 +102,19 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
const [customCodeModalVisible, setCustomCodeModalVisible] = useState(false);
// Content Filter data ref (managed by ContentFilterManager)
const contentFilterDataRef = React.useRef<{ patterns: any[]; blockedWords: any[] }>({
const contentFilterDataRef = React.useRef<{
patterns: any[];
blockedWords: any[];
categories: any[];
}>({
patterns: [],
blockedWords: [],
categories: [],
});
// Memoize onDataChange callback to prevent unnecessary re-renders
const handleContentFilterDataChange = useCallback((patterns: any[], blockedWords: any[]) => {
contentFilterDataRef.current = { patterns, blockedWords };
const handleContentFilterDataChange = useCallback((patterns: any[], blockedWords: any[], categories: any[]) => {
contentFilterDataRef.current = { patterns, blockedWords, categories: categories || [] };
}, []);
const fetchGuardrailInfo = async () => {
@@ -274,16 +283,15 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
// Only add Content Filter patterns if there are changes
if (guardrailData.litellm_params?.guardrail === "litellm_content_filter" && hasUnsavedContentFilterChanges) {
const originalPatterns = guardrailData.litellm_params?.patterns || [];
const originalBlockedWords = guardrailData.litellm_params?.blocked_words || [];
const formattedData = formatContentFilterDataForAPI(
contentFilterDataRef.current.patterns || [],
contentFilterDataRef.current.blockedWords || [],
contentFilterDataRef.current.categories || [],
);
updateData.litellm_params.patterns = formattedData.patterns;
updateData.litellm_params.blocked_words = formattedData.blocked_words;
updateData.litellm_params.categories = formattedData.categories;
}
if (guardrailData.litellm_params?.guardrail === "tool_permission") {
@@ -354,7 +362,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
console.log("allowedParams: ", allowedParams);
allowedParams.forEach((paramName) => {
if (paramName === "patterns" || paramName === "blocked_words") {
if (paramName === "patterns" || paramName === "blocked_words" || paramName === "categories") {
return;
}
// Check for both direct parameter name and nested optional_params object
@@ -438,9 +446,9 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
return (
<div className="p-4">
<div>
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
<Button type="text" icon={<ArrowLeftIcon className="w-4 h-4" />} onClick={onClose} className="mb-4">
Back to Guardrails
</TremorButton>
</Button>
<Title>{guardrailData.guardrail_name || "Unnamed Guardrail"}</Title>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">{guardrailData.guardrail_id}</Text>
@@ -450,11 +458,10 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
size="small"
icon={copiedStates["guardrail-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(guardrailData.guardrail_id, "guardrail-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["guardrail-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
className={`left-2 z-10 transition-all duration-200 ${copiedStates["guardrail-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</div>
</div>
@@ -533,9 +540,8 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
<Text className="flex-1 font-medium text-gray-900">{key}</Text>
<Text className="flex-1">
<span
className={`inline-flex items-center gap-1.5 ${
value === "MASK" ? "text-blue-600" : "text-red-600"
}`}
className={`inline-flex items-center gap-1.5 ${value === "MASK" ? "text-blue-600" : "text-red-600"
}`}
>
{value === "MASK" ? <EyeInvisibleOutlined /> : <StopOutlined />}
{String(value)}
@@ -563,14 +569,13 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
<Text className="font-medium text-lg">Custom Code</Text>
</div>
{isAdmin && !isConfigGuardrail && (
<TremorButton
size="xs"
variant="secondary"
icon={CodeOutlined}
<Button
size="small"
icon={<CodeOutlined />}
onClick={() => setCustomCodeModalVisible(true)}
>
Edit Code
</TremorButton>
</Button>
)}
</div>
<div className="relative rounded-lg overflow-hidden border border-gray-700 bg-[#1e1e1e]">
@@ -603,14 +608,14 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
)}
{!isEditing && !isConfigGuardrail && (
guardrailData.litellm_params?.guardrail === "custom_code" ? (
<TremorButton
icon={CodeOutlined}
<Button
icon={<CodeOutlined />}
onClick={() => setCustomCodeModalVisible(true)}
>
Edit Code
</TremorButton>
</Button>
) : (
<TremorButton onClick={() => setIsEditing(true)}>Edit Settings</TremorButton>
<Button onClick={() => setIsEditing(true)}>Edit Settings</Button>
)
)}
</div>
@@ -637,7 +642,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
name="guardrail_name"
rules={[{ required: true, message: "Please input a guardrail name" }]}
>
<TextInput />
<Input placeholder="Enter guardrail name" />
</Form.Item>
<Form.Item label="Default On" name="default_on">
@@ -675,7 +680,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
onUnsavedChanges={setHasUnsavedContentFilterChanges}
/>
<Divider orientation="left">Provider Settings</Divider>
{(guardrailData.litellm_params?.guardrail === "tool_permission" || guardrailProviderSpecificParams) && <Divider orientation="left">Provider Settings</Divider>}
{guardrailData.litellm_params?.guardrail === "tool_permission" ? (
<ToolPermissionRulesEditor
@@ -735,7 +740,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
>
Cancel
</Button>
<TremorButton>Save Changes</TremorButton>
<Button type="primary" htmlType="submit">Save Changes</Button>
</div>
</Form>
) : (
@@ -1,6 +1,5 @@
import React from "react";
import { Form, Select, Typography } from "antd";
import { TextInput } from "@tremor/react";
import { Form, Select, Typography, Input, Button } from "antd";
import NumericalInput from "../shared/numerical_input";
const { Title } = Typography;
@@ -95,17 +94,18 @@ const DictField: React.FC<DictFieldProps> = ({ field, fieldKey, fullFieldKey, va
<Select.Option value={false}>False</Select.Option>
</Select>
) : (
<TextInput placeholder={`Enter ${entry.key} value`} type="text" />
<Input placeholder={`Enter ${entry.key} value`} />
)}
</Form.Item>
</div>
<button
type="button"
className="text-red-500 hover:text-red-700 text-sm"
<Button
type="text"
danger
size="small"
onClick={() => removeEntry(entry.id, entry.key)}
>
Remove
</button>
</Button>
</div>
))}
@@ -198,9 +198,9 @@ const GuardrailOptionalParams: React.FC<GuardrailOptionalParamsProps> = ({
) : field.type === "number" ? (
<NumericalInput step={1} width={400} placeholder={field.description} />
) : fieldKey.includes("password") || fieldKey.includes("secret") || fieldKey.includes("key") ? (
<TextInput placeholder={field.description} type="password" />
<Input.Password placeholder={field.description} />
) : (
<TextInput placeholder={field.description} type="text" />
<Input placeholder={field.description} />
)}
</Form.Item>
</div>
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import { Form, Select, Spin } from "antd";
import { TextInput } from "@tremor/react";
import { Form, Select, Spin, Input } from "antd";
import {
guardrail_provider_map,
populateGuardrailProviders,
@@ -195,9 +194,9 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
defaultValue={fieldValue !== undefined ? Number(fieldValue) : undefined}
/>
) : fieldKey.includes("password") || fieldKey.includes("secret") || fieldKey.includes("key") ? (
<TextInput placeholder={field.description} type="password" defaultValue={fieldValue || ""} />
<Input.Password placeholder={field.description} defaultValue={fieldValue || ""} />
) : (
<TextInput placeholder={field.description} type="text" defaultValue={fieldValue || ""} />
<Input placeholder={field.description} defaultValue={fieldValue || ""} />
)}
</Form.Item>
);