From 3bd304e061492c58d2cb9c181b94300582d532de Mon Sep 17 00:00:00 2001 From: nishika26 Date: Fri, 10 Apr 2026 20:08:15 +0530 Subject: [PATCH 01/12] Guardrails UI: Configure and list validators --- .env.example | 6 +- app/(main)/coming-soon/guardrails/page.tsx | 14 - .../configurations/prompt-editor/page.tsx | 7 + app/(main)/guardrails/page.tsx | 429 ++++++++++++ app/api/apikeys/verify/route.ts | 14 + .../ban_lists/[ban_list_id]/route.ts | 56 ++ app/api/guardrails/ban_lists/route.ts | 30 + app/api/guardrails/route.ts | 27 + .../validators/configs/[config_id]/route.ts | 95 +++ .../guardrails/validators/configs/route.ts | 61 ++ app/components/InfoTooltip.tsx | 42 ++ app/components/MultiSelect.tsx | 142 ++++ app/components/Sidebar.tsx | 6 + app/components/guardrails/BanListModal.tsx | 187 ++++++ .../guardrails/ValidatorConfigPanel.tsx | 608 ++++++++++++++++++ app/components/guardrails/types.ts | 44 ++ app/components/guardrails/validators.json | 42 ++ app/components/icons/index.tsx | 1 + .../icons/sidebar/ShieldCheckIcon.tsx | 22 + .../prompt-editor/ConfigEditorPane.tsx | 212 +++++- app/lib/apiClient.ts | 35 +- app/lib/types/configs.ts | 9 + 22 files changed, 2072 insertions(+), 17 deletions(-) delete mode 100644 app/(main)/coming-soon/guardrails/page.tsx create mode 100644 app/(main)/guardrails/page.tsx create mode 100644 app/api/apikeys/verify/route.ts create mode 100644 app/api/guardrails/ban_lists/[ban_list_id]/route.ts create mode 100644 app/api/guardrails/ban_lists/route.ts create mode 100644 app/api/guardrails/route.ts create mode 100644 app/api/guardrails/validators/configs/[config_id]/route.ts create mode 100644 app/api/guardrails/validators/configs/route.ts create mode 100644 app/components/InfoTooltip.tsx create mode 100644 app/components/MultiSelect.tsx create mode 100644 app/components/guardrails/BanListModal.tsx create mode 100644 app/components/guardrails/ValidatorConfigPanel.tsx create mode 100644 app/components/guardrails/types.ts create mode 100644 app/components/guardrails/validators.json create mode 100644 app/components/icons/sidebar/ShieldCheckIcon.tsx diff --git a/.env.example b/.env.example index ae81a04d..7a71f2b8 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,5 @@ -BACKEND_URL=http://localhost:8000 \ No newline at end of file +BACKEND_URL=http://localhost:8000 + +#for guardrails +GUARDRAILS_URL = http://localhost:8001 +GUARDRAILS_TOKEN = diff --git a/app/(main)/coming-soon/guardrails/page.tsx b/app/(main)/coming-soon/guardrails/page.tsx deleted file mode 100644 index 771d9666..00000000 --- a/app/(main)/coming-soon/guardrails/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Guardrails - Coming Soon Page - */ - -import ComingSoon from "@/app/components/ComingSoon"; - -export default function GuardrailsPage() { - return ( - - ); -} diff --git a/app/(main)/configurations/prompt-editor/page.tsx b/app/(main)/configurations/prompt-editor/page.tsx index 55a65dbb..67c6997d 100644 --- a/app/(main)/configurations/prompt-editor/page.tsx +++ b/app/(main)/configurations/prompt-editor/page.tsx @@ -294,6 +294,12 @@ function PromptEditorContent() { }), }, }, + ...(currentConfigBlob.input_guardrails?.length && { + input_guardrails: currentConfigBlob.input_guardrails, + }), + ...(currentConfigBlob.output_guardrails?.length && { + output_guardrails: currentConfigBlob.output_guardrails, + }), }; const existingConfigMeta = allConfigMeta.find( @@ -502,6 +508,7 @@ function PromptEditorContent() { isSaving={isSaving} collapsed={!showConfigPane} onToggle={() => setShowConfigPane(!showConfigPane)} + apiKey={activeKey?.key ?? ""} /> diff --git a/app/(main)/guardrails/page.tsx b/app/(main)/guardrails/page.tsx new file mode 100644 index 00000000..8c26066e --- /dev/null +++ b/app/(main)/guardrails/page.tsx @@ -0,0 +1,429 @@ +/** + * Guardrails — 2-panel layout: + * [LEFT: Config Form] | [RIGHT: Saved Configs List] + */ + +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import Sidebar from "@/app/components/Sidebar"; +import { colors } from "@/app/lib/colors"; +import { useApp } from "@/app/lib/context/AppContext"; +import { useAuth } from "@/app/lib/context/AuthContext"; +import { useToast } from "@/app/components/Toast"; +import { apiFetch } from "@/app/lib/apiClient"; +import PageHeader from "@/app/components/PageHeader"; +import { Validator, SavedValidatorConfig, formatValidatorName } from "@/app/components/guardrails/types"; +import ValidatorConfigPanel from "@/app/components/guardrails/ValidatorConfigPanel"; +import { TrashIcon } from "@/app/components/icons"; +import validatorMeta from "@/app/components/guardrails/validators.json"; + +interface OrgContext { + organization_id: number; + project_id: number; +} + +interface ValidatorMeta { + validator_type: string; + validator_name: string; + description: string; +} + +const metaMap: Record = (validatorMeta as ValidatorMeta[]).reduce( + (acc, v) => ({ ...acc, [v.validator_type]: v }), + {}, +); + +export default function GuardrailsPage() { + const { sidebarCollapsed } = useApp(); + const { activeKey, isHydrated } = useAuth(); + const toast = useToast(); + + // Org/project context from API key verification + const [orgContext, setOrgContext] = useState(null); + + // Available validators from API + const [validators, setValidators] = useState([]); + const [validatorsLoading, setValidatorsLoading] = useState(true); + + // Saved configs from API + const [savedConfigs, setSavedConfigs] = useState([]); + const [savedConfigsLoading, setSavedConfigsLoading] = useState(true); + + // Form state + const [selectedValidatorType, setSelectedValidatorType] = useState(null); + const [selectedSavedConfig, setSelectedSavedConfig] = useState(null); + const [isSaving, setIsSaving] = useState(false); + + // Step 1: verify API key → get org/project IDs + useEffect(() => { + if (!isHydrated) return; + if (!activeKey?.key) { + toast.error("No API key found. Please add your Kaapi API key in the Keystore."); + return; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + apiFetch("/api/apikeys/verify", activeKey.key) + .then((data) => { + const org_id = data?.data?.organization_id; + const proj_id = data?.data?.project_id; + if (org_id != null && proj_id != null) { + setOrgContext({ organization_id: org_id, project_id: proj_id }); + } else { + toast.error("Could not determine organization/project from API key"); + } + }) + .catch((e: Error) => toast.error(e.message || "API key verification failed — check your key in the Keystore")); + }, [isHydrated, activeKey?.key]); + + // Step 2: fetch available validators (no auth needed for catalog) + useEffect(() => { + setValidatorsLoading(true); + fetch("/api/guardrails") + .then((r) => r.json()) + .then((data) => { + const list: Validator[] = Array.isArray(data?.validators) ? data.validators : []; + setValidators(list); + }) + .catch(() => toast.error("Failed to load validators")) + .finally(() => setValidatorsLoading(false)); + }, []); + + // Step 3: fetch saved configs once we have org/project context + const configsQueryString = orgContext + ? `?organization_id=${parseInt(String(orgContext.organization_id), 10)}&project_id=${parseInt(String(orgContext.project_id), 10)}` + : null; + + const fetchSavedConfigs = useCallback(() => { + if (!configsQueryString) return; + setSavedConfigsLoading(true); + fetch(`/api/guardrails/validators/configs${configsQueryString}`) + .then((r) => r.json()) + .then((data) => { + const list: SavedValidatorConfig[] = Array.isArray(data?.data?.configs) + ? data.data.configs + : Array.isArray(data?.data) + ? data.data + : Array.isArray(data?.configs) + ? data.configs + : Array.isArray(data) + ? data + : []; + setSavedConfigs(list); + }) + .catch(() => toast.error("Failed to load saved configs")) + .finally(() => setSavedConfigsLoading(false)); + }, [configsQueryString]); + + useEffect(() => { + fetchSavedConfigs(); + }, [fetchSavedConfigs]); + + // Load a saved config into the form + const handleSelectSavedConfig = (cfg: SavedValidatorConfig) => { + setSelectedSavedConfig(cfg); + setSelectedValidatorType(cfg.type); + }; + + // Reset the form + const handleClearForm = () => { + setSelectedValidatorType(null); + setSelectedSavedConfig(null); + }; + + const handleDeleteConfig = async (configId: string) => { + if (!configsQueryString) return; + try { + const res = await fetch( + `/api/guardrails/validators/configs/${configId}${configsQueryString}`, + { method: "DELETE" }, + ); + if (!res.ok) throw new Error("Delete failed"); + toast.success("Config deleted"); + if (selectedSavedConfig?.id === configId) { + handleClearForm(); + } + fetchSavedConfigs(); + } catch { + toast.error("Failed to delete config"); + } + }; + + const handleSaveConfig = async (name: string, configValues: Record) => { + if (!name.trim()) { + toast.error("Please enter a config name"); + return; + } + if (!configsQueryString) { + toast.error("API key not verified yet"); + return; + } + setIsSaving(true); + try { + const isUpdate = !!selectedSavedConfig; + const url = isUpdate + ? `/api/guardrails/validators/configs/${selectedSavedConfig!.id}${configsQueryString}` + : `/api/guardrails/validators/configs${configsQueryString}`; + + // PATCH only accepts these five fields — strip everything else + const body = isUpdate + ? { + name: configValues.name, + type: configValues.type, + stage: configValues.stage, + on_fail_action: configValues.on_fail_action, + is_enabled: configValues.is_enabled, + } + : configValues; + + const res = await fetch(url, { + method: isUpdate ? "PATCH" : "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err?.error ?? (isUpdate ? "Update failed" : "Save failed")); + } + toast.success(isUpdate ? `Config "${name}" updated` : `Config "${name}" saved`); + fetchSavedConfigs(); + setSelectedSavedConfig(null); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Failed to save config"); + } finally { + setIsSaving(false); + } + }; + + const existingValues = selectedSavedConfig + ? { + ...(selectedSavedConfig.config ?? {}), + stage: selectedSavedConfig.stage, + on_fail_action: selectedSavedConfig.on_fail_action, + is_enabled: selectedSavedConfig.is_enabled, + } + : null; + + return ( +
+ + +
+ {/* Page header */} + + + {/* 2-panel body */} +
+ {/* LEFT: Config form panel */} +
+ +
+ + {/* RIGHT: Saved configs list */} +
+ +
+
+
+
+ ); +} + +// ─── Saved Configs List Panel ──────────────────────────────────────────────── + +interface SavedConfigsListProps { + configs: SavedValidatorConfig[]; + isLoading: boolean; + selectedConfigId: string | null; + onSelectConfig: (cfg: SavedValidatorConfig) => void; + onDeleteConfig: (id: string) => void; + onNewConfig: () => void; +} + +function SavedConfigsList({ + configs, + isLoading, + selectedConfigId, + onSelectConfig, + onDeleteConfig, + onNewConfig, +}: SavedConfigsListProps) { + return ( +
+ {/* Panel header */} +
+
+
+ Saved Configurations +
+ {!isLoading && ( +
+ {configs.length} config{configs.length !== 1 ? 's' : ''} +
+ )} +
+ +
+ + {/* List */} +
+ {isLoading ? ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ) : configs.length === 0 ? ( +
+ + + +

+ No saved configurations yet +

+

+ Select a validator type on the left and save your first config +

+
+ ) : ( +
+ {configs.map((cfg) => { + const isSelected = selectedConfigId === cfg.id; + const displayName = metaMap[cfg.type]?.validator_name ?? formatValidatorName(cfg.type); + return ( +
onSelectConfig(cfg)} + > +
+ {/* Text + badges */} +
+
+ {cfg.name} +
+
+ + {displayName} + + {cfg.stage && ( + + {cfg.stage} + + )} + {cfg.on_fail_action && ( + + on fail: {cfg.on_fail_action} + + )} + {cfg.is_enabled === false && ( + + disabled + + )} +
+
+ + {/* Action buttons */} +
+ + +
+
+
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/app/api/apikeys/verify/route.ts b/app/api/apikeys/verify/route.ts new file mode 100644 index 00000000..faf3ee54 --- /dev/null +++ b/app/api/apikeys/verify/route.ts @@ -0,0 +1,14 @@ +import { apiClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +export async function GET(request: NextRequest) { + try { + const { status, data } = await apiClient(request, "/api/v1/apikeys/verify"); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} diff --git a/app/api/guardrails/ban_lists/[ban_list_id]/route.ts b/app/api/guardrails/ban_lists/[ban_list_id]/route.ts new file mode 100644 index 00000000..ae164757 --- /dev/null +++ b/app/api/guardrails/ban_lists/[ban_list_id]/route.ts @@ -0,0 +1,56 @@ +import { guardrailsClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ ban_list_id: string }> }, +) { + try { + const { ban_list_id } = await params; + const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${ban_list_id}`); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ ban_list_id: string }> }, +) { + try { + const { ban_list_id } = await params; + const body = await request.json(); + const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${ban_list_id}`, { + method: "PUT", + body: JSON.stringify(body), + }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ ban_list_id: string }> }, +) { + try { + const { ban_list_id } = await params; + const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${ban_list_id}`, { + method: "DELETE", + }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} diff --git a/app/api/guardrails/ban_lists/route.ts b/app/api/guardrails/ban_lists/route.ts new file mode 100644 index 00000000..10306d34 --- /dev/null +++ b/app/api/guardrails/ban_lists/route.ts @@ -0,0 +1,30 @@ +import { guardrailsClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +export async function GET(request: NextRequest) { + try { + const { status, data } = await guardrailsClient(request, "/api/v1/guardrails/ban_lists"); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { status, data } = await guardrailsClient(request, "/api/v1/guardrails/ban_lists", { + method: "POST", + body: JSON.stringify(body), + }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} diff --git a/app/api/guardrails/route.ts b/app/api/guardrails/route.ts new file mode 100644 index 00000000..2aa92372 --- /dev/null +++ b/app/api/guardrails/route.ts @@ -0,0 +1,27 @@ +import { guardrailsClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +function getAuthHeader(): string | undefined { + const token = process.env.GUARDRAILS_TOKEN; + return token ? `Bearer ${token}` : undefined; +} + + +export async function GET(request: NextRequest) { + const authHeader = getAuthHeader(); + if (!authHeader) { + return NextResponse.json( + { error: "Missing GUARDRAILS_TOKEN environment variable" }, + { status: 500 }, + ); + } + try { + const { status, data } = await guardrailsClient(request,"/api/v1/guardrails/" , { authHeader }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} \ No newline at end of file diff --git a/app/api/guardrails/validators/configs/[config_id]/route.ts b/app/api/guardrails/validators/configs/[config_id]/route.ts new file mode 100644 index 00000000..ce92c75d --- /dev/null +++ b/app/api/guardrails/validators/configs/[config_id]/route.ts @@ -0,0 +1,95 @@ +import { guardrailsClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +function getAuthHeader(): string | undefined { + const token = process.env.GUARDRAILS_TOKEN; + return token ? `Bearer ${token}` : undefined; +} + +function buildEndpoint(request: NextRequest, config_id: string): string { + const { searchParams } = new URL(request.url); + const params = new URLSearchParams(); + const organizationId = searchParams.get("organization_id"); + const projectId = searchParams.get("project_id"); + if (organizationId) params.append("organization_id", organizationId); + if (projectId) params.append("project_id", projectId); + const qs = params.toString(); + return `/api/v1/guardrails/validators/configs/${config_id}${qs ? `?${qs}` : ""}`; +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ config_id: string }> }, +) { + const authHeader = getAuthHeader(); + if (!authHeader) { + return NextResponse.json( + { error: "Missing GUARDRAILS_TOKEN environment variable" }, + { status: 500 }, + ); + } + try { + const { config_id } = await params; + const { status, data } = await guardrailsClient(request, buildEndpoint(request, config_id), { authHeader }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ config_id: string }> }, +) { + const authHeader = getAuthHeader(); + if (!authHeader) { + return NextResponse.json( + { error: "Missing GUARDRAILS_TOKEN environment variable" }, + { status: 500 }, + ); + } + try { + const { config_id } = await params; + const body = await request.json(); + const { status, data } = await guardrailsClient(request, buildEndpoint(request, config_id), { + method: "PATCH", + body: JSON.stringify(body), + authHeader, + }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ config_id: string }> }, +) { + const authHeader = getAuthHeader(); + if (!authHeader) { + return NextResponse.json( + { error: "Missing GUARDRAILS_TOKEN environment variable" }, + { status: 500 }, + ); + } + try { + const { config_id } = await params; + const { status, data } = await guardrailsClient(request, buildEndpoint(request, config_id), { + method: "DELETE", + authHeader, + }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} diff --git a/app/api/guardrails/validators/configs/route.ts b/app/api/guardrails/validators/configs/route.ts new file mode 100644 index 00000000..1c3ccf6a --- /dev/null +++ b/app/api/guardrails/validators/configs/route.ts @@ -0,0 +1,61 @@ +import { guardrailsClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +function getAuthHeader(): string | undefined { + const token = process.env.GUARDRAILS_TOKEN; + return token ? `Bearer ${token}` : undefined; +} + +function buildEndpoint(request: NextRequest): string { + const { searchParams } = new URL(request.url); + const params = new URLSearchParams(); + const organizationId = searchParams.get("organization_id"); + const projectId = searchParams.get("project_id"); + if (organizationId) params.append("organization_id", organizationId); + if (projectId) params.append("project_id", projectId); + const qs = params.toString(); + return `/api/v1/guardrails/validators/configs${qs ? `?${qs}` : ""}`; +} + +export async function GET(request: NextRequest) { + const authHeader = getAuthHeader(); + if (!authHeader) { + return NextResponse.json( + { error: "Missing GUARDRAILS_TOKEN environment variable" }, + { status: 500 }, + ); + } + try { + const { status, data } = await guardrailsClient(request, buildEndpoint(request), { authHeader }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function POST(request: NextRequest) { + const authHeader = getAuthHeader(); + if (!authHeader) { + return NextResponse.json( + { error: "Missing GUARDRAILS_TOKEN environment variable" }, + { status: 500 }, + ); + } + try { + const body = await request.json(); + const { status, data } = await guardrailsClient(request, buildEndpoint(request), { + method: "POST", + body: JSON.stringify(body), + authHeader, + }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} diff --git a/app/components/InfoTooltip.tsx b/app/components/InfoTooltip.tsx new file mode 100644 index 00000000..977e4a8c --- /dev/null +++ b/app/components/InfoTooltip.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useState } from "react"; +import { colors } from "@/app/lib/colors"; + +interface InfoTooltipProps { + text: string; +} + +export default function InfoTooltip({ text }: InfoTooltipProps) { + const [visible, setVisible] = useState(false); + return ( + + + {visible && ( +
+ {text} +
+ )} +
+ ); +} diff --git a/app/components/MultiSelect.tsx b/app/components/MultiSelect.tsx new file mode 100644 index 00000000..85d6bc5e --- /dev/null +++ b/app/components/MultiSelect.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { colors } from "@/app/lib/colors"; + +interface MultiSelectProps { + options: string[]; + value: string[]; + onChange: (value: string[]) => void; + placeholder?: string; +} + +export default function MultiSelect({ options, value, onChange, placeholder }: MultiSelectProps) { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, []); + + const toggle = (opt: string) => { + if (value.includes(opt)) { + onChange(value.filter((v) => v !== opt)); + } else { + onChange([...value, opt]); + } + }; + + const remove = (opt: string, e: React.MouseEvent) => { + e.stopPropagation(); + onChange(value.filter((v) => v !== opt)); + }; + + const unselected = options.filter((o) => !value.includes(o)); + + return ( +
+ {/* Input box with tags */} +
setOpen((v) => !v)} + > + {/* Selected tags */} + {value.map((v) => ( + e.stopPropagation()} + > + {v} + + + ))} + + {/* Placeholder when nothing selected */} + {value.length === 0 && ( + + {placeholder ?? "Select options…"} + + )} + + {/* Chevron */} + + + +
+ + {/* Dropdown list */} + {open && ( +
+ {unselected.length === 0 ? ( +

+ All options selected +

+ ) : ( + unselected.map((opt) => ( + + )) + )} +
+ )} +
+ ); +} diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index 63d00aa2..4e4af967 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -14,6 +14,7 @@ import { GearIcon, SlidersIcon, KeyIcon, + ShieldCheckIcon, ChevronRightIcon, } from "@/app/components/icons"; @@ -91,6 +92,11 @@ export default function Sidebar({ { name: "Prompt Editor", route: "/configurations/prompt-editor" }, ], }, + { + name: "Guardrails", + route: "/guardrails", + icon: + }, { name: "Settings", route: "/settings/credentials", diff --git a/app/components/guardrails/BanListModal.tsx b/app/components/guardrails/BanListModal.tsx new file mode 100644 index 00000000..2a8e7537 --- /dev/null +++ b/app/components/guardrails/BanListModal.tsx @@ -0,0 +1,187 @@ +import { useState } from 'react'; +import { colors } from '@/app/lib/colors'; + +interface BanListModalProps { + onClose: () => void; + onCreated: (banList: { id: string; name: string }) => void; + apiKey: string; +} + +export default function BanListModal({ onClose, onCreated, apiKey }: BanListModalProps) { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [bannedWords, setBannedWords] = useState(''); + const [domain, setDomain] = useState(''); + const [isPublic, setIsPublic] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(''); + + const handleCreate = async () => { + if (!name.trim()) { setError('Name is required'); return; } + if (!bannedWords.trim()) { setError('At least one banned word is required'); return; } + + setError(''); + setIsSaving(true); + try { + const body = { + name: name.trim(), + description: description.trim(), + banned_words: bannedWords.split(',').map((w) => w.trim()).filter(Boolean), + domain: domain.trim(), + is_public: isPublic, + }; + const res = await fetch('/api/guardrails/ban_lists', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-API-KEY': apiKey }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err?.error ?? 'Failed to create ban list'); + } + const data = await res.json(); + onCreated({ id: data.id, name: data.name ?? name.trim() }); + } catch (e) { + setError(e instanceof Error ? e.message : 'Something went wrong'); + } finally { + setIsSaving(false); + } + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+
+

+ Create Ban List +

+

+ Define a list of words to ban from outputs +

+
+ +
+ + {/* Body */} +
+ {error && ( +

+ {error} +

+ )} + +
+ + setName(e.target.value)} + placeholder="e.g. profanity-list" + className="w-full text-sm rounded-md border px-2.5 py-1.5 outline-none focus:ring-1" + style={{ borderColor: colors.border, backgroundColor: colors.bg.primary, color: colors.text.primary }} + /> +
+ +
+ +