feat(ui): guardrails/content_filter batch to shadcn

- KeywordModal / CustomPatternModal: antd Modal/Select/Space/Button/
  Input \u2192 shadcn Dialog/Select/Button/Input/Textarea/Label. Footer
  buttons use outline + default variants.
- KeywordTable: antd Table/Select/Button + @ant-design DeleteOutlined
  \u2192 shadcn Table/Select/Button + lucide Trash2. Delete column is a
  destructive-tinted ghost button.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 12:03:06 +00:00
parent ca3226bc42
commit 6d50a85b98
No known key found for this signature in database
3 changed files with 207 additions and 171 deletions

View file

@ -1,8 +1,21 @@
import React from "react";
import { Typography, Select, Modal, Space, Button, Input } from "antd";
const { Text } = Typography;
const { Option } = Select;
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface CustomPatternModalProps {
visible: boolean;
@ -28,64 +41,67 @@ const CustomPatternModal: React.FC<CustomPatternModalProps> = ({
onCancel,
}) => {
return (
<Modal
title="Add custom regex pattern"
<Dialog
open={visible}
onCancel={onCancel}
footer={null}
width={800}
onOpenChange={(o) => (!o ? onCancel() : undefined)}
>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div>
<Text strong>Pattern name</Text>
<Input
placeholder="e.g., internal_id, employee_code"
value={patternName}
onChange={(e) => onNameChange(e.target.value)}
style={{ marginTop: 8 }}
/>
</div>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>Add custom regex pattern</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-6">
<div>
<Label className="font-bold">Pattern name</Label>
<Input
placeholder="e.g., internal_id, employee_code"
value={patternName}
onChange={(e) => onNameChange(e.target.value)}
className="mt-2"
/>
</div>
<div>
<Text strong>Regex pattern</Text>
<Input
placeholder="e.g., ID-[0-9]{6}"
value={patternRegex}
onChange={(e) => onRegexChange(e.target.value)}
style={{ marginTop: 8 }}
/>
<Text type="secondary" style={{ fontSize: 12 }}>
Enter a valid regular expression to match sensitive data
</Text>
</div>
<div>
<Label className="font-bold">Regex pattern</Label>
<Input
placeholder="e.g., ID-[0-9]{6}"
value={patternRegex}
onChange={(e) => onRegexChange(e.target.value)}
className="mt-2"
/>
<p className="text-xs text-muted-foreground mt-1">
Enter a valid regular expression to match sensitive data
</p>
</div>
<div>
<Text strong>Action</Text>
<Text type="secondary" style={{ display: "block", marginTop: 4, marginBottom: 8 }}>
Choose what action the guardrail should take when this pattern is detected
</Text>
<Select
value={patternAction}
onChange={onActionChange}
style={{ width: "100%" }}
>
<Option value="BLOCK">Block</Option>
<Option value="MASK">Mask</Option>
</Select>
<div>
<Label className="font-bold">Action</Label>
<p className="text-sm text-muted-foreground mt-1 mb-2">
Choose what action the guardrail should take when this pattern is
detected
</p>
<Select
value={patternAction}
onValueChange={(v) => onActionChange(v as "BLOCK" | "MASK")}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BLOCK">Block</SelectItem>
<SelectItem value="MASK">Mask</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</Space>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "24px" }}>
<Button onClick={onCancel}>
Cancel
</Button>
<Button type="primary" onClick={onAdd}>
Add
</Button>
</div>
</Modal>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button onClick={onAdd}>Add</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default CustomPatternModal;

View file

@ -1,8 +1,22 @@
import React from "react";
import { Typography, Select, Modal, Space, Button, Input } from "antd";
const { Text } = Typography;
const { Option } = Select;
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
interface KeywordModalProps {
visible: boolean;
@ -28,62 +42,65 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
onCancel,
}) => {
return (
<Modal
title="Add blocked keyword"
<Dialog
open={visible}
onCancel={onCancel}
footer={null}
width={800}
onOpenChange={(o) => (!o ? onCancel() : undefined)}
>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div>
<Text strong>Keyword</Text>
<Input
placeholder="Enter sensitive keyword or phrase"
value={keyword}
onChange={(e) => onKeywordChange(e.target.value)}
style={{ marginTop: 8 }}
/>
</div>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>Add blocked keyword</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-6">
<div>
<Label className="font-bold">Keyword</Label>
<Input
placeholder="Enter sensitive keyword or phrase"
value={keyword}
onChange={(e) => onKeywordChange(e.target.value)}
className="mt-2"
/>
</div>
<div>
<Text strong>Action</Text>
<Text type="secondary" style={{ display: "block", marginTop: 4, marginBottom: 8 }}>
Choose what action the guardrail should take when this keyword is detected
</Text>
<Select
value={action}
onChange={onActionChange}
style={{ width: "100%" }}
>
<Option value="BLOCK">Block</Option>
<Option value="MASK">Mask</Option>
</Select>
</div>
<div>
<Label className="font-bold">Action</Label>
<p className="text-sm text-muted-foreground mt-1 mb-2">
Choose what action the guardrail should take when this keyword is
detected
</p>
<Select
value={action}
onValueChange={(v) => onActionChange(v as "BLOCK" | "MASK")}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BLOCK">Block</SelectItem>
<SelectItem value="MASK">Mask</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Text strong>Description (optional)</Text>
<Input.TextArea
placeholder="Explain why this keyword is sensitive"
value={description}
onChange={(e) => onDescriptionChange(e.target.value)}
rows={3}
style={{ marginTop: 8 }}
/>
<div>
<Label className="font-bold">Description (optional)</Label>
<Textarea
placeholder="Explain why this keyword is sensitive"
value={description}
onChange={(e) => onDescriptionChange(e.target.value)}
rows={3}
className="mt-2"
/>
</div>
</div>
</Space>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "24px" }}>
<Button onClick={onCancel}>
Cancel
</Button>
<Button type="primary" onClick={onAdd}>
Add
</Button>
</div>
</Modal>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button onClick={onAdd}>Add</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default KeywordModal;

View file

@ -1,10 +1,22 @@
import { DeleteOutlined } from "@ant-design/icons";
import { Button, Select, Table, Typography } from "antd";
import { Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import React from "react";
const { Text } = Typography;
const { Option } = Select;
interface BlockedWord {
id: string;
keyword: string;
@ -14,6 +26,7 @@ interface BlockedWord {
interface KeywordTableProps {
keywords: BlockedWord[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onActionChange: (id: string, field: string, value: any) => void;
onRemove: (id: string) => void;
}
@ -23,71 +36,61 @@ const KeywordTable: React.FC<KeywordTableProps> = ({
onActionChange,
onRemove,
}) => {
const columns = [
{
title: "Keyword",
dataIndex: "keyword",
key: "keyword",
},
{
title: "Action",
dataIndex: "action",
key: "action",
width: 150,
render: (action: string, record: BlockedWord) => (
<Select
value={action}
onChange={(value) => onActionChange(record.id, "action", value)}
style={{ width: 120 }}
size="small"
>
<Option value="BLOCK">Block</Option>
<Option value="MASK">Mask</Option>
</Select>
),
},
{
title: "Description",
dataIndex: "description",
key: "description",
render: (desc: string) => desc || "-",
},
{
title: "",
key: "actions",
width: 100,
render: (_: any, record: BlockedWord) => (
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => onRemove(record.id)}
>
Delete
</Button>
),
},
];
if (keywords.length === 0) {
return (
<div style={{ textAlign: "center", padding: "40px 0", color: "#999" }}>
<div className="text-center py-10 text-muted-foreground">
No keywords added.
</div>
);
}
return (
<Table
dataSource={keywords}
columns={columns}
rowKey="id"
pagination={false}
size="small"
/>
<div className="border border-border rounded-md overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Keyword</TableHead>
<TableHead className="w-[150px]">Action</TableHead>
<TableHead>Description</TableHead>
<TableHead className="w-[100px]" />
</TableRow>
</TableHeader>
<TableBody>
{keywords.map((record) => (
<TableRow key={record.id}>
<TableCell>{record.keyword}</TableCell>
<TableCell>
<Select
value={record.action}
onValueChange={(v) => onActionChange(record.id, "action", v)}
>
<SelectTrigger className="w-[120px] h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BLOCK">Block</SelectItem>
<SelectItem value="MASK">Mask</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>{record.description || "-"}</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => onRemove(record.id)}
>
<Trash2 className="h-3.5 w-3.5" />
Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
export default KeywordTable;