Add aviation and UAE policy templates with tag-based filtering (#21518)

* add aviation and UAE regex patterns (PNR, Skywards, Emirates ID, UAE phone, flight number)

* add aviation safety topics category yaml

* add airline brand protection category yaml

* add UAE cultural sensitivity category yaml

* add UAE anti-discrimination category yaml

* add aviation, UAE, and competitor policy templates + migrate region/type to tags

* sync policy_templates_backup.json

* add /policy/templates/enrich endpoint for LLM competitor discovery

* add enrichPolicyTemplate networking call

* add TemplateParameterModal component

* handle parameterized templates and LLM enrichment in policies index

* replace region/type radio filters with tag sidebar checkboxes
This commit is contained in:
Ishaan Jaff 2026-02-18 18:26:24 -08:00 • committed by GitHub
parent a385126a7c
commit 91fd2b4621
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 2798 additions and 364 deletions

File diff suppressed because it is too large Load diff

View file

@ -452,6 +452,47 @@
"pattern": "\\b\\d{1,6}\\s+[A-Za-z0-9][A-Za-z0-9\\s.'-]*\\s+(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Drive|Dr|Lane|Ln|Way|Court|Ct|Place|Pl|Circle|Cir)\\b",
"category": "PII Patterns",
"description": "Detects street addresses (number + street name + street type)"
},
{
"name": "airline_pnr",
"display_name": "Airline PNR / Booking Reference",
"pattern": "\\b[A-Z]{6}\\b",
"category": "Aviation PII Patterns",
"description": "Detects airline PNR / booking references (6 uppercase alpha characters) when near booking context",
"keyword_pattern": "\\b(?:PNR|booking\\s*(?:reference|ref|code|number|confirmation)|reservation\\s*(?:code|number|ref)|record\\s*locator|confirmation\\s*(?:code|number)|itinerary\\s*(?:number|ref))\\b",
"allow_word_numbers": false
},
{
"name": "skywards_number",
"display_name": "Emirates Skywards / Frequent Flyer Number",
"pattern": "\\b(?:EK\\s?)?\\d{9,10}\\b",
"category": "Aviation PII Patterns",
"description": "Detects Emirates Skywards frequent flyer numbers (9-10 digits, optional EK prefix) when near loyalty/frequent flyer context",
"keyword_pattern": "\\b(?:[Ss]kywards|frequent\\s*flyer|FF\\s*(?:number|no|#)|loyalty\\s*(?:number|id|member)|member\\s*(?:number|id|#)|miles\\s*(?:account|number)|tier\\s*(?:number|status))\\b",
"allow_word_numbers": false
},
{
"name": "uae_emirates_id",
"display_name": "UAE Emirates ID",
"pattern": "\\b784-?\\d{4}-?\\d{7}-?\\d\\b",
"category": "UAE PII Patterns",
"description": "Detects UAE Emirates ID numbers (784-YYYY-NNNNNNN-C format, 15 digits starting with 784)"
},
{
"name": "uae_phone",
"display_name": "Phone Number (UAE)",
"pattern": "(?<!\\d)(?:\\+971|00971|0)\\s?(?:2|3|4|6|7|9|50|52|54|55|56|58)\\s?\\d{3}\\s?\\d{4}\\b",
"category": "UAE PII Patterns",
"description": "Detects UAE phone numbers (+971/00971/0 prefix, landline and mobile)"
},
{
"name": "flight_number",
"display_name": "Airline Flight Number",
"pattern": "\\b(?:EK|QF|BA|AA|LH|AF|SQ|CX|QR|EY|FZ|WY|GF|KU|SV|MS|RJ|ME|PK|AI|UL|WS|9W|6E)\\s?\\d{1,4}\\b",
"category": "Aviation PII Patterns",
"description": "Detects airline flight numbers (major IATA 2-letter codes + 1-4 digit flight number) when near flight context",
"keyword_pattern": "\\b(?:flight|departure|arrival|gate|boarding|schedule|operate|route|aircraft|plane|outbound|inbound|leg|sector|flying)\\b",
"allow_word_numbers": false
}
]
}

View file

@ -0,0 +1,89 @@
# Airline Brand Protection
# Prevents AI from generating fake incident reports, unauthorized statements, or negative PR
# NOTE: The identifier_words use {{brand_name}} placeholder - this file is used as a reference.
# The actual brand-specific keywords are injected at template instantiation time via blocked_words.
category_name: "airline_brand_protection"
description: "Prevents AI from generating fake incident reports, unauthorized airline statements, or reputation-damaging content"
default_action: "BLOCK"
identifier_words:
# Generic airline context (brand-specific terms injected at runtime)
- "airline"
- "carrier"
- "airways"
- "flights"
additional_block_words:
# Fake incident reports
- "crashed"
- "crash landing"
- "emergency landing"
- "engine failure"
- "engine fire"
- "depressurization"
- "hull breach"
- "mayday"
- "mid-air collision"
- "near miss"
- "fatality"
- "fatalities"
- "dead passengers"
- "casualties"
# Unauthorized statements
- "official statement"
- "spokesperson said"
- "press release"
- "confirms"
- "announces"
- "announced today"
- "breaking news"
- "just in"
- "BREAKING"
- "statement from"
# PR / reputation attacks
- "scandal"
- "cover up"
- "coverup"
- "lawsuit"
- "class action"
- "mistreatment"
- "safety violation"
- "whistleblower"
- "banned"
- "grounded fleet"
- "recall"
- "boycott"
- "worst airline"
- "unsafe"
- "dangerous"
- "negligence"
always_block_keywords:
- keyword: "plane crash"
severity: "high"
- keyword: "flight crashed"
severity: "high"
- keyword: "mid-air collision"
severity: "high"
- keyword: "passengers dead"
severity: "high"
- keyword: "confirms fatalities"
severity: "high"
- keyword: "safety scandal"
severity: "high"
- keyword: "fleet grounded"
severity: "high"
exceptions:
- "how safe is"
- "safety record"
- "safety rating"
- "what is"
- "explain"
- "historical"
- "aviation history"
- "customer review"
- "customer service"
- "feedback"
- "improve"
- "complaint resolution"

View file

@ -0,0 +1,120 @@
# Aviation Safety & Security Topic Restrictions
# Prevents AI from generating content about aircraft vulnerabilities, security bypass, cockpit access
category_name: "aviation_safety_topics"
description: "Blocks AI from generating content about aviation security vulnerabilities, bypass procedures, and restricted operations data"
default_action: "BLOCK"
identifier_words:
- "aircraft"
- "airplane"
- "plane"
- "aviation"
- "airline"
- "airport"
- "cockpit"
- "flight deck"
- "airside"
- "tarmac"
- "runway"
- "cargo hold"
- "fuselage"
- "avionics"
- "transponder"
- "ATC"
- "air traffic control"
- "ACARS"
- "flight management system"
- "FMS"
- "autopilot"
- "A380"
- "B777"
- "777"
- "787"
additional_block_words:
# Security bypass
- "bypass security"
- "bypass screening"
- "defeat security"
- "circumvent security"
- "evade detection"
- "avoid screening"
- "smuggle"
- "smuggling"
- "conceal weapon"
- "hidden weapon"
# Cockpit access
- "cockpit door code"
- "cockpit access code"
- "override cockpit"
- "force cockpit door"
- "break into cockpit"
- "cockpit intrusion"
- "cockpit breach"
# Aircraft vulnerabilities
- "vulnerability"
- "exploit"
- "disable"
- "jamming"
- "spoofing"
- "hijack"
- "hijacking"
- "take over"
- "commandeer"
- "crash intentionally"
- "bring down"
- "shoot down"
- "sabotage"
- "tamper"
- "interfere with"
# System exploitation
- "hack"
- "hack into"
- "inject malware"
- "remote access"
- "unauthorized access"
always_block_keywords:
- keyword: "hijack a plane"
severity: "high"
- keyword: "hijack an aircraft"
severity: "high"
- keyword: "cockpit door override code"
severity: "high"
- keyword: "bypass airport security"
severity: "high"
- keyword: "smuggle weapons onto aircraft"
severity: "high"
- keyword: "disable aircraft transponder"
severity: "high"
- keyword: "bring down an aircraft"
severity: "high"
- keyword: "crash a plane into"
severity: "high"
- keyword: "aircraft bomb"
severity: "high"
- keyword: "ACARS vulnerability"
severity: "high"
- keyword: "flight management system hack"
severity: "high"
- keyword: "spoof ADS-B"
severity: "high"
exceptions:
- "aviation safety"
- "safety report"
- "safety investigation"
- "safety improvement"
- "NTSB"
- "accident investigation"
- "safety training"
- "security awareness"
- "safety management system"
- "IATA safety"
- "ICAO"
- "how to report"
- "incident report"
- "safety culture"
- "what is"
- "explain"
- "research on"

View file

@ -0,0 +1,71 @@
# UAE Anti-Discrimination Compliance
# UAE Federal Decree-Law No. 2/2015 on combating discrimination and hatred
category_name: "uae_anti_discrimination"
description: "Blocks content violating UAE Federal anti-discrimination laws (Decree-Law No. 2/2015)"
default_action: "BLOCK"
identifier_words:
- "Emirati"
- "Emiratis"
- "expatriate"
- "expat"
- "migrant worker"
- "laborer"
- "domestic worker"
- "nationality"
- "religion"
- "race"
- "ethnicity"
- "caste"
- "sect"
additional_block_words:
- "inferior"
- "superior"
- "subhuman"
- "uncivilized"
- "backward"
- "primitive"
- "servant class"
- "slave"
- "slavery"
- "second class"
- "second-class citizen"
- "don't belong"
- "go back to"
- "deport"
- "hate"
- "hatred"
- "incite"
- "incitement"
- "sedition"
- "sectarian"
always_block_keywords:
- keyword: "racial superiority"
severity: "high"
- keyword: "ethnic cleansing"
severity: "high"
- keyword: "sectarian violence"
severity: "high"
- keyword: "incite hatred"
severity: "high"
- keyword: "incite discrimination"
severity: "high"
- keyword: "religious hatred"
severity: "high"
exceptions:
- "anti-discrimination"
- "equal rights"
- "diversity and inclusion"
- "tolerance"
- "coexistence"
- "UAE Year of Tolerance"
- "human rights"
- "what is"
- "explain"
- "history of"
- "education"
- "awareness"
- "compliance training"

View file

@ -0,0 +1,72 @@
# UAE Cultural Sensitivity
# Respects UAE cultural norms, royal family references, and religious content policies
category_name: "uae_cultural_sensitivity"
description: "Ensures AI output respects UAE cultural norms, royal family, and religious content policies"
default_action: "BLOCK"
identifier_words:
- "UAE"
- "United Arab Emirates"
- "Dubai"
- "Abu Dhabi"
- "Sharjah"
- "Ajman"
- "Fujairah"
- "Ras Al Khaimah"
- "Umm Al Quwain"
- "Emirati"
- "Emiratis"
additional_block_words:
# Royal family disrespect
- "mock"
- "ridicule"
- "insult"
- "satirize"
- "caricature"
- "parody"
- "defame"
- "slander"
- "disrespect"
# Cultural sensitivity
- "alcohol promotion"
- "gambling promotion"
- "drug use"
- "immodest"
- "indecent"
always_block_keywords:
- keyword: "insult the ruler"
severity: "high"
- keyword: "mock the sheikh"
severity: "high"
- keyword: "ridicule the royal family"
severity: "high"
- keyword: "defame Mohammed bin"
severity: "high"
- keyword: "blasphemy against Islam"
severity: "high"
- keyword: "insult Islam"
severity: "high"
- keyword: "mock the Prophet"
severity: "high"
- keyword: "anti-Islamic"
severity: "high"
- keyword: "desecrate the Quran"
severity: "high"
exceptions:
- "what is"
- "explain"
- "cultural guide"
- "travel guide"
- "etiquette"
- "customs"
- "tradition"
- "how to respect"
- "cultural awareness"
- "history of"
- "learn about"
- "understand"
- "policy on"
- "regulation"

View file

@ -14,7 +14,7 @@ import os
from typing import TYPE_CHECKING, List, Literal, Optional, TypedDict, cast
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from pydantic import BaseModel
from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_guardrail import CustomGuardrail
@ -507,3 +507,137 @@ async def get_policy_templates(
)
return _load_policy_templates_from_local_backup()
class EnrichTemplateRequest(BaseModel):
template_id: str
parameters: dict
@router.post(
"/policy/templates/enrich",
tags=["policy management"],
dependencies=[Depends(user_api_key_auth)],
)
@management_endpoint_wrapper
async def enrich_policy_template(
data: EnrichTemplateRequest,
request: Request,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
) -> dict:
"""
Enrich a policy template with LLM-discovered data (e.g. competitor names).
Calls an onboarded LLM to discover competitors for the given brand name,
then returns enriched guardrailDefinitions with the discovered data populated.
"""
templates = _load_policy_templates_from_local_backup()
template = next((t for t in templates if t.get("id") == data.template_id), None)
if template is None:
raise HTTPException(status_code=404, detail=f"Template '{data.template_id}' not found")
llm_enrichment = template.get("llm_enrichment")
if llm_enrichment is None:
raise HTTPException(
status_code=400,
detail="Template does not support LLM enrichment",
)
brand_name = data.parameters.get(llm_enrichment["parameter"], "")
if not brand_name:
raise HTTPException(
status_code=400,
detail=f"Parameter '{llm_enrichment['parameter']}' is required",
)
prompt = llm_enrichment["prompt"].replace(
"{{" + llm_enrichment["parameter"] + "}}", brand_name
)
competitors = await _discover_competitors_via_llm(prompt)
enriched_definitions = _build_competitor_guardrail_definitions(
template.get("guardrailDefinitions", []),
competitors,
brand_name,
)
return {"guardrailDefinitions": enriched_definitions, "competitors": competitors}
async def _discover_competitors_via_llm(prompt: str) -> list:
"""Call an onboarded LLM to discover competitor names."""
import litellm
try:
response = await litellm.acompletion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
)
raw = response.choices[0].message.content or "" # type: ignore
competitors = [
line.strip().strip(".-) ").strip()
for line in raw.strip().split("\n")
if line.strip() and len(line.strip()) > 1
]
return competitors[:15]
except Exception as e:
verbose_proxy_logger.error("LLM competitor discovery failed: %s", e)
return []
def _build_competitor_guardrail_definitions(
definitions: list,
competitors: list,
brand_name: str,
) -> list:
"""Build enriched guardrailDefinitions with competitor names populated."""
import copy
enriched = copy.deepcopy(definitions)
output_blocked = [
{"keyword": comp, "action": "BLOCK", "description": f"Competitor: {comp}"}
for comp in competitors
]
recommendation_blocked = []
for comp in competitors:
recommendation_blocked.append(
{"keyword": f"try {comp}", "action": "BLOCK", "description": "Recommendation to competitor"}
)
recommendation_blocked.append(
{"keyword": f"use {comp}", "action": "BLOCK", "description": "Recommendation to competitor"}
)
recommendation_blocked.append(
{"keyword": f"switch to {comp}", "action": "BLOCK", "description": "Recommendation to competitor"}
)
recommendation_blocked.append(
{"keyword": f"consider {comp}", "action": "BLOCK", "description": "Recommendation to competitor"}
)
comparison_blocked = []
for comp in competitors:
comparison_blocked.append(
{"keyword": f"{comp} is better", "action": "BLOCK", "description": "Unfavorable comparison"}
)
comparison_blocked.append(
{"keyword": f"better than {brand_name}", "action": "BLOCK", "description": "Unfavorable comparison"}
)
comparison_blocked.append(
{"keyword": f"{brand_name} is worse", "action": "BLOCK", "description": "Unfavorable comparison"}
)
blocked_words_map = {
"competitor-output-blocker": output_blocked,
"competitor-recommendation-filter": recommendation_blocked,
"competitor-comparison-filter": comparison_blocked,
}
for defn in enriched:
guardrail_name = defn.get("guardrail_name", "")
if guardrail_name in blocked_words_map:
defn["litellm_params"]["blocked_words"] = blocked_words_map[guardrail_name]
return enriched

File diff suppressed because it is too large Load diff

View file

@ -5552,6 +5552,39 @@ export const getPolicyTemplates = async (accessToken: string) => {
}
};
export const enrichPolicyTemplate = async (
accessToken: string,
templateId: string,
parameters: Record<string, string>
) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/policy/templates/enrich`
: `/policy/templates/enrich`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ template_id: templateId, parameters }),
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to enrich policy template:", error);
throw error;
}
};
export const createPolicyCall = async (accessToken: string, policyData: any) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies` : `/policies`;

View file

@ -12,6 +12,7 @@ import AddAttachmentForm from "./add_attachment_form";
import PolicyTestPanel from "./policy_test_panel";
import PolicyTemplates from "./policy_templates";
import GuardrailSelectionModal from "./guardrail_selection_modal";
import TemplateParameterModal from "./template_parameter_modal";
import {
getPoliciesList,
deletePolicyCall,
@ -23,6 +24,7 @@ import {
updatePolicyCall,
createPolicyAttachmentCall,
createGuardrailCall,
enrichPolicyTemplate,
} from "../networking";
import {
Policy,
@ -58,6 +60,9 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
const [existingGuardrailNames, setExistingGuardrailNames] = useState<Set<string>>(new Set());
const [isCreatingGuardrails, setIsCreatingGuardrails] = useState(false);
const [showFlowBuilder, setShowFlowBuilder] = useState(false);
const [isParameterModalOpen, setIsParameterModalOpen] = useState(false);
const [isEnrichingTemplate, setIsEnrichingTemplate] = useState(false);
const [pendingTemplate, setPendingTemplate] = useState<any>(null);
const isAdmin = userRole ? isAdminRole(userRole) : false;
@ -187,8 +192,20 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
return;
}
// If template has parameters, show parameter modal first
if (template.parameters && template.parameters.length > 0) {
setPendingTemplate(template);
setIsParameterModalOpen(true);
return;
}
await proceedWithTemplate(template);
};
const proceedWithTemplate = async (template: any) => {
if (!accessToken) return;
try {
// Fetch existing guardrails to show in the modal
const existingGuardrailsResponse = await getGuardrailsList(accessToken);
const existingNames = new Set<string>(
existingGuardrailsResponse.guardrails?.map((g: any) => g.guardrail_name as string) || []
@ -203,6 +220,53 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
}
};
const substituteParameters = (template: any, parameters: Record<string, string>): any => {
let templateStr = JSON.stringify(template);
for (const [key, value] of Object.entries(parameters)) {
templateStr = templateStr.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
}
return JSON.parse(templateStr);
};
const handleParameterConfirm = async (parameters: Record<string, string>) => {
if (!accessToken || !pendingTemplate) return;
setIsEnrichingTemplate(true);
try {
let enrichedTemplate = pendingTemplate;
if (pendingTemplate.llm_enrichment) {
// Call backend to enrich template with LLM-discovered data
const enrichResult = await enrichPolicyTemplate(
accessToken,
pendingTemplate.id,
parameters
);
// The backend returns the enriched guardrailDefinitions
enrichedTemplate = { ...pendingTemplate, guardrailDefinitions: enrichResult.guardrailDefinitions };
}
// Substitute parameters in template
enrichedTemplate = substituteParameters(enrichedTemplate, parameters);
setIsParameterModalOpen(false);
setIsEnrichingTemplate(false);
setPendingTemplate(null);
await proceedWithTemplate(enrichedTemplate);
} catch (error) {
console.error("Error enriching template:", error);
message.error("Failed to configure template. Please try again.");
setIsEnrichingTemplate(false);
}
};
const handleParameterCancel = () => {
setIsParameterModalOpen(false);
setPendingTemplate(null);
};
const handleGuardrailSelectionConfirm = async (selectedGuardrailDefinitions: any[]) => {
if (!accessToken || !selectedTemplate) return;
@ -420,6 +484,14 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
onCancel={handleGuardrailSelectionCancel}
isLoading={isCreatingGuardrails}
/>
<TemplateParameterModal
visible={isParameterModalOpen}
template={pendingTemplate}
onConfirm={handleParameterConfirm}
onCancel={handleParameterCancel}
isLoading={isEnrichingTemplate}
/>
</TabPanel>
<TabPanel>

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useMemo } from "react";
import { Card, Button, Spin, message, Radio } from "antd";
import { Card, Button, Spin, message, Checkbox, Badge } from "antd";
import {
ShieldCheckIcon,
ShieldExclamationIcon,
@ -16,6 +16,7 @@ interface PolicyTemplateCardProps {
iconColor: string;
iconBg: string;
guardrails: string[];
tags: string[];
inherits?: string;
complexity: "Low" | "Medium" | "High";
onUseTemplate: () => void;
@ -28,6 +29,7 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
iconColor,
iconBg,
guardrails,
tags,
inherits,
complexity,
onUseTemplate,
@ -60,7 +62,20 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
</div>
<h3 className="text-base font-semibold text-gray-900 mb-2">{title}</h3>
<p className="text-sm text-gray-500 mb-6 flex-grow">{description}</p>
<p className="text-sm text-gray-500 mb-4 flex-grow">{description}</p>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-4">
{tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 border border-blue-100"
>
{tag}
</span>
))}
</div>
)}
{inherits && (
<div className="mb-4 text-xs">
@ -116,26 +131,45 @@ const iconMap: Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>
const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, accessToken }) => {
const [templates, setTemplates] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedRegion, setSelectedRegion] = useState<string>("All");
const [selectedType, setSelectedType] = useState<string>("All");
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
const availableRegions = useMemo(() => {
const regions = new Set(templates.map(t => t.region || "Global"));
return ["All", ...Array.from(regions).sort()];
}, [templates]);
const availableTypes = useMemo(() => {
const types = new Set(templates.map(t => t.type || "General"));
return ["All", ...Array.from(types).sort()];
}, [templates]);
const filteredTemplates = useMemo(() => {
return templates.filter(t => {
const regionMatch = selectedRegion === "All" || (t.region || "Global") === selectedRegion;
const typeMatch = selectedType === "All" || (t.type || "General") === selectedType;
return regionMatch && typeMatch;
// Compute all unique tags with counts
const tagCounts = useMemo(() => {
const counts: Record<string, number> = {};
templates.forEach((t) => {
const tags: string[] = t.tags || [];
tags.forEach((tag: string) => {
counts[tag] = (counts[tag] || 0) + 1;
});
});
}, [templates, selectedRegion, selectedType]);
// Sort alphabetically
return Object.entries(counts).sort(([a], [b]) => a.localeCompare(b));
}, [templates]);
// Filter templates: show templates that have ALL selected tags (AND logic)
const filteredTemplates = useMemo(() => {
if (selectedTags.size === 0) return templates;
return templates.filter((t) => {
const tags: string[] = t.tags || [];
return Array.from(selectedTags).every((selectedTag) => tags.includes(selectedTag));
});
}, [templates, selectedTags]);
const handleTagToggle = (tag: string) => {
setSelectedTags((prev) => {
const next = new Set(prev);
if (next.has(tag)) {
next.delete(tag);
} else {
next.add(tag);
}
return next;
});
};
const handleClearAll = () => {
setSelectedTags(new Set());
};
useEffect(() => {
const fetchTemplates = async () => {
@ -178,54 +212,88 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, access
</div>
</div>
<div className="flex items-center gap-6 mb-4">
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-gray-700">Region:</span>
<Radio.Group
value={selectedRegion}
onChange={(e) => setSelectedRegion(e.target.value)}
buttonStyle="solid"
>
{availableRegions.map(region => (
<Radio.Button key={region} value={region}>
{region}
</Radio.Button>
))}
</Radio.Group>
</div>
{availableTypes.length > 2 && (
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-gray-700">Type:</span>
<Radio.Group
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
buttonStyle="solid"
>
{availableTypes.map(type => (
<Radio.Button key={type} value={type}>
{type}
</Radio.Button>
))}
</Radio.Group>
<div className="flex gap-6">
{/* Left sidebar - tag filters */}
{tagCounts.length > 0 && (
<div className="w-52 flex-shrink-0">
<div className="sticky top-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-gray-900">
Categories
</span>
{selectedTags.size > 0 && (
<button
onClick={handleClearAll}
className="text-xs text-blue-600 hover:text-blue-800"
>
Clear all
</button>
)}
</div>
<div className="space-y-1">
{tagCounts.map(([tag, count]) => (
<label
key={tag}
className={`flex items-center justify-between px-2 py-1.5 rounded-md cursor-pointer transition-colors ${
selectedTags.has(tag)
? "bg-blue-50"
: "hover:bg-gray-50"
}`}
>
<div className="flex items-center gap-2">
<Checkbox
checked={selectedTags.has(tag)}
onChange={() => handleTagToggle(tag)}
/>
<span className="text-sm text-gray-700">{tag}</span>
</div>
<span className="text-xs text-gray-400 font-medium">
{count}
</span>
</label>
))}
</div>
</div>
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredTemplates.map((template, index) => (
<PolicyTemplateCard
key={template.id || index}
title={template.title}
description={template.description}
icon={iconMap[template.icon] || ShieldCheckIcon}
iconColor={template.iconColor}
iconBg={template.iconBg}
guardrails={template.guardrails}
inherits={template.inherits}
complexity={template.complexity}
onUseTemplate={() => onUseTemplate(template)}
/>
))}
{/* Right content - template cards */}
<div className="flex-1">
{selectedTags.size > 0 && (
<div className="mb-4 text-sm text-gray-500">
Showing {filteredTemplates.length} of {templates.length} templates
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredTemplates.map((template, index) => (
<PolicyTemplateCard
key={template.id || index}
title={template.title}
description={template.description}
icon={iconMap[template.icon] || ShieldCheckIcon}
iconColor={template.iconColor}
iconBg={template.iconBg}
guardrails={template.guardrails}
tags={template.tags || []}
inherits={template.inherits}
complexity={template.complexity}
onUseTemplate={() => onUseTemplate(template)}
/>
))}
</div>
{filteredTemplates.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p>No templates match the selected filters.</p>
<button
onClick={handleClearAll}
className="text-blue-600 hover:text-blue-800 mt-2 text-sm"
>
Clear all filters
</button>
</div>
)}
</div>
</div>
</div>
);

View file

@ -0,0 +1,127 @@
import React, { useState, useEffect } from "react";
import { Modal, Spin } from "antd";
import { Button, TextInput } from "@tremor/react";
interface TemplateParameter {
name: string;
label: string;
type: string;
required: boolean;
placeholder?: string;
}
interface TemplateParameterModalProps {
visible: boolean;
template: any;
onConfirm: (parameters: Record<string, string>) => void;
onCancel: () => void;
isLoading?: boolean;
}
const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
visible,
template,
onConfirm,
onCancel,
isLoading = false,
}) => {
const [parameterValues, setParameterValues] = useState<Record<string, string>>({});
const parameters: TemplateParameter[] = template?.parameters || [];
useEffect(() => {
if (visible && template) {
const initial: Record<string, string> = {};
parameters.forEach((p) => {
initial[p.name] = "";
});
setParameterValues(initial);
}
}, [visible, template]);
const allRequiredFilled = parameters
.filter((p) => p.required)
.every((p) => (parameterValues[p.name] || "").trim().length > 0);
const handleConfirm = () => {
onConfirm(parameterValues);
};
return (
<Modal
title={
<div>
<h3 className="text-lg font-semibold mb-1">{template?.title}</h3>
<p className="text-sm text-gray-500 font-normal">
{template?.llm_enrichment
? "Enter your brand name to auto-discover competitors and configure guardrails"
: "Configure template parameters"}
</p>
</div>
}
open={visible}
onCancel={onCancel}
width={500}
footer={[
<Button key="cancel" variant="secondary" onClick={onCancel} disabled={isLoading}>
Cancel
</Button>,
<Button
key="confirm"
onClick={handleConfirm}
loading={isLoading}
disabled={!allRequiredFilled || isLoading}
>
{isLoading
? template?.llm_enrichment
? "Discovering competitors..."
: "Processing..."
: "Continue"}
</Button>,
]}
>
<div className="py-4 space-y-4">
{parameters.map((param) => (
<div key={param.name}>
<label className="block text-sm font-medium text-gray-700 mb-1">
{param.label}
{param.required && <span className="text-red-500 ml-1">*</span>}
</label>
<TextInput
placeholder={param.placeholder || ""}
value={parameterValues[param.name] || ""}
onChange={(e) =>
setParameterValues((prev) => ({
...prev,
[param.name]: e.target.value,
}))
}
/>
</div>
))}
{template?.llm_enrichment && (
<div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-100">
<p className="text-sm text-blue-800">
This template uses AI to automatically discover your competitors and configure
guardrails. An onboarded LLM will be called to identify competitor names.
</p>
</div>
)}
{isLoading && (
<div className="flex items-center gap-3 mt-4 p-3 bg-gray-50 rounded-lg">
<Spin size="small" />
<span className="text-sm text-gray-600">
{template?.llm_enrichment
? "Using AI to discover competitors..."
: "Processing template..."}
</span>
</div>
)}
</div>
</Modal>
);
};
export default TemplateParameterModal;