diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..218111c --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +NEXT_PUBLIC_BACKEND_URL= +NEXT_PUBLIC_GUARDRAILS_URL = + +GUARDRAILS_TOKEN= \ No newline at end of file diff --git a/app/api/apikeys/verify/route.ts b/app/api/apikeys/verify/route.ts new file mode 100644 index 0000000..43eb335 --- /dev/null +++ b/app/api/apikeys/verify/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + +export async function GET(request: NextRequest) { + try { + // Get the API key from request headers + const apiKey = request.headers.get('X-API-KEY'); + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + // Forward the request to the actual backend + const url = `${backendUrl}/api/v1/apikeys/verify`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : {}; + + // Return the response with the same status code + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file 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 0000000..8a0dae7 --- /dev/null +++ b/app/api/guardrails/ban_lists/[ban_list_id]/route.ts @@ -0,0 +1,157 @@ +import { NextResponse } from 'next/server'; + +const backendUrl = process.env.NEXT_PUBLIC_GUARDRAILS_URL || 'http://localhost:8001'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ ban_list_id: string }> } +) { + const { ban_list_id } = await params; + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + try { + const url = `${backendUrl}/api/v1/guardrails/ban_lists/${ban_list_id}`; + + console.log('[GET /api/guardrails/ban_lists/[ban_list_id]] Forwarding to:', url); + + const response = await fetch(url, { + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + console.log('[GET /api/guardrails/ban_list/[ban_list_id]] Backend response status:', response.status, response.statusText); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : {}; + + console.log('[GET /api/guardrails/ban_list/[ban_list_id]] Backend response data:', JSON.stringify(data, null, 2)); + + if (!response.ok) { + console.error('[GET /api/guardrails/ban_list/[ban_list_id]] Backend error:', response.status, data); + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { status: 500 } + ); + } +} + +export async function PUT( + request: Request, + { params }: { params: Promise<{ ban_list_id: string }> } +) { + const { ban_list_id } = await params; + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + try { + // Get the JSON body from the request + const body = await request.json(); + + const url = `${backendUrl}/api/v1/guardrails/ban_lists/${ban_list_id}`; + + console.log('[PUT /api/guardrails/ban_list/[ban_list_id]] Forwarding to:', url); + console.log('[PUT /api/guardrails/ban_list/[ban_list_id]] Body:', JSON.stringify(body, null, 2)); + + const response = await fetch(url, { + method: 'PUT', + body: JSON.stringify(body), + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + console.log('[PUT /api/guardrails/ban_list/[ban_list_id]] Backend response status:', response.status, response.statusText); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : { success: true }; + + console.log('[PUT /api/guardrails/ban_list/[ban_list_id]] Backend response data:', JSON.stringify(data, null, 2)); + + if (!response.ok) { + console.error('[PUT /api/guardrails/ban_list/[ban_list_id]] Backend error:', response.status, data); + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { status: 500 } + ); + } +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ ban_list_id: string }> } +) { + const { ban_list_id } = await params; + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + try { + const url = `${backendUrl}/api/v1/guardrails/ban_lists/${ban_list_id}`; + + console.log('[DELETE /api/guardrails/ban_lists/[ban_list_id]] Forwarding to:', url); + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + console.log('[DELETE /api/guardrails/ban_list/[ban_list_id]] Backend response status:', response.status, response.statusText); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : { success: true }; + + console.log('[DELETE /api/guardrails/ban_list/[ban_list_id]] Backend response data:', JSON.stringify(data, null, 2)); + + if (!response.ok) { + console.error('[DELETE /api/guardrails/ban_list/[ban_list_id]] Backend error:', response.status, data); + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/guardrails/ban_lists/route.ts b/app/api/guardrails/ban_lists/route.ts new file mode 100644 index 0000000..a79fac2 --- /dev/null +++ b/app/api/guardrails/ban_lists/route.ts @@ -0,0 +1,104 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const backendUrl = process.env.NEXT_PUBLIC_GUARDRAILS_URL || 'http://localhost:8001'; + +export async function GET(request: NextRequest) { + try { + // Get the Kaapi API key from request headers + const apiKey = request.headers.get('X-API-KEY'); + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + const url = `${backendUrl}/api/v1/guardrails/ban_lists`; + + console.log('[GET /api/guardrails/ban_list] Forwarding to:', url); + + // Forward the request to the actual backend + const response = await fetch(url, { + method: 'GET', + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + console.log('[GET /api/guardrails/ban_list] Backend response status:', response.status, response.statusText); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : { data: [] }; + + console.log('[GET /api/guardrails/ban_list] Backend response data:', JSON.stringify(data, null, 2)); + + // Return the response with the same status code + if (!response.ok) { + console.error('[GET /api/guardrails/ban_list] Backend error:', response.status, data); + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + // Get the Kaapi API key from request headers + const apiKey = request.headers.get('X-API-KEY'); + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + // Get the JSON body from the request + const body = await request.json(); + + const url = `${backendUrl}/api/v1/guardrails/ban_lists`; + + console.log('[POST /api/guardrails/ban_list] Forwarding to:', url); + console.log('[POST /api/guardrails/ban_list] Body:', JSON.stringify(body, null, 2)); + + // Forward the request to the actual backend + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + console.log('[POST /api/guardrails/ban_list] Backend response status:', response.status, response.statusText); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : { success: true }; + + console.log('[POST /api/guardrails/ban_list] Backend response data:', JSON.stringify(data, null, 2)); + + // Return the response with the same status code + if (!response.ok) { + console.error('[POST /api/guardrails/ban_list] Backend error:', response.status, data); + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { 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 0000000..1fac4ea --- /dev/null +++ b/app/api/guardrails/validators/configs/[config_id]/route.ts @@ -0,0 +1,110 @@ +import { NextResponse } from 'next/server'; + + +const backendUrl = process.env.NEXT_PUBLIC_GUARDRAILS_URL || 'http://localhost:8001'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ config_id: string }> } +) { + const { config_id } = await params; + + // Get the guardrails token from environment variable + const guardrailsToken = process.env.GUARDRAILS_TOKEN; + if (!guardrailsToken) { + return NextResponse.json( + { error: 'Missing GUARDRAILS_TOKEN environment variable' }, + { status: 500 } + ); + } + + const authHeader = `Bearer ${guardrailsToken}`; + + try { + // Get query parameters + const { searchParams } = new URL(request.url); + const organizationId = searchParams.get('organization_id'); + const projectId = searchParams.get('project_id'); + + // Build query string + const queryParams = new URLSearchParams(); + if (organizationId) queryParams.append('organization_id', organizationId); + if (projectId) queryParams.append('project_id', projectId); + + const queryString = queryParams.toString(); + const url = `${backendUrl}/api/v1/guardrails/validators/configs/${config_id}${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + headers: { + 'Authorization': authHeader, + 'Content-Type': 'application/json', + }, + }); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : {}; + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { status: 500 } + ); + } +} + + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ config_id: string }> } +) { + const { config_id } = await params; + + // Get the guardrails token from environment variable + const guardrailsToken = process.env.GUARDRAILS_TOKEN; + if (!guardrailsToken) { + return NextResponse.json( + { error: 'Missing GUARDRAILS_TOKEN environment variable' }, + { status: 500 } + ); + } + + const authHeader = `Bearer ${guardrailsToken}`; + + try { + // Get query parameters + const { searchParams } = new URL(request.url); + const organizationId = searchParams.get('organization_id'); + const projectId = searchParams.get('project_id'); + + // Build query string + const queryParams = new URLSearchParams(); + if (organizationId) queryParams.append('organization_id', organizationId); + if (projectId) queryParams.append('project_id', projectId); + + const queryString = queryParams.toString(); + const url = `${backendUrl}/api/v1/guardrails/validators/configs/${config_id}${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Authorization': authHeader, + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + return NextResponse.json( + { success: false, error: 'Failed to delete config', data: null }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/guardrails/validators/configs/route.ts b/app/api/guardrails/validators/configs/route.ts new file mode 100644 index 0000000..2a620ee --- /dev/null +++ b/app/api/guardrails/validators/configs/route.ts @@ -0,0 +1,131 @@ +import { NextRequest, NextResponse } from 'next/server'; + + +const backendUrl = process.env.NEXT_PUBLIC_GUARDRAILS_URL || 'http://localhost:8001'; + +export async function GET(request: NextRequest) { + try { + // Get the guardrails token from environment variable + const guardrailsToken = process.env.GUARDRAILS_TOKEN; + if (!guardrailsToken) { + return NextResponse.json( + { error: 'Missing GUARDRAILS_TOKEN environment variable' }, + { status: 500 } + ); + } + + const authHeader = `Bearer ${guardrailsToken}`; + + // Get query parameters + const { searchParams } = new URL(request.url); + const organizationId = searchParams.get('organization_id'); + const projectId = searchParams.get('project_id'); + + // Build query string + const queryParams = new URLSearchParams(); + if (organizationId) queryParams.append('organization_id', organizationId); + if (projectId) queryParams.append('project_id', projectId); + + const queryString = queryParams.toString(); + const url = `${backendUrl}/api/v1/guardrails/validators/configs${queryString ? `?${queryString}` : ''}`; + + console.log('[GET /api/guardrails/validators/configs] Fetching from:', url); + + // Forward the request to the actual backend + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': authHeader, + 'Content-Type': 'application/json', + }, + }); + + console.log('[GET /api/guardrails/validators/configs] Backend response status:', response.status, response.statusText); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : { success: true, data: [] }; + + console.log('[GET /api/guardrails/validators/configs] Backend response data:', JSON.stringify(data, null, 2)); + + // Return the response with the same status code + if (!response.ok) { + console.error('[GET /api/guardrails/validators/configs] Backend error:', response.status, data); + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + // Get the guardrails token from environment variable + const guardrailsToken = process.env.GUARDRAILS_TOKEN; + if (!guardrailsToken) { + return NextResponse.json( + { error: 'Missing GUARDRAILS_TOKEN environment variable' }, + { status: 500 } + ); + } + + const authHeader = `Bearer ${guardrailsToken}`; + + // Get the JSON body from the request + const body = await request.json(); + + // Get query parameters + const { searchParams } = new URL(request.url); + const organizationId = searchParams.get('organization_id'); + const projectId = searchParams.get('project_id'); + + // Build query string + const queryParams = new URLSearchParams(); + if (organizationId) queryParams.append('organization_id', organizationId); + if (projectId) queryParams.append('project_id', projectId); + + const queryString = queryParams.toString(); + const url = `${backendUrl}/api/v1/guardrails/validators/configs${queryString ? `?${queryString}` : ''}`; + + console.log('[POST /api/guardrails/validators/configs] Forwarding to:', url); + console.log('[POST /api/guardrails/validators/configs] Body:', JSON.stringify(body, null, 2)); + + // Forward the request to the actual backend + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Authorization': authHeader, + 'Content-Type': 'application/json', + }, + }); + + console.log('[POST /api/guardrails/validators/configs] Backend response status:', response.status, response.statusText); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : { success: true }; + + console.log('[POST /api/guardrails/validators/configs] Backend response data:', JSON.stringify(data, null, 2)); + + // Return the response with the same status code + if (!response.ok) { + console.error('[POST /api/guardrails/validators/configs] Backend error:', response.status, data); + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/coming-soon/guardrails/page.tsx b/app/coming-soon/guardrails/page.tsx deleted file mode 100644 index 1e4ab0f..0000000 --- a/app/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/components/Sidebar.tsx b/app/components/Sidebar.tsx index 7105146..564e8ab 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -96,6 +96,7 @@ export default function Sidebar({ collapsed, activeRoute = '/evaluations' }: Sid submenu: [ { name: 'Library', route: '/configurations' }, { name: 'Prompt Editor', route: '/configurations/prompt-editor' }, + { name: 'Safety Guardrails', route: '/configurations/safety-guardrails'} ] } ]; diff --git a/app/components/prompt-editor/ConfigEditorPane.tsx b/app/components/prompt-editor/ConfigEditorPane.tsx index 0c43c9c..2997ee0 100644 --- a/app/components/prompt-editor/ConfigEditorPane.tsx +++ b/app/components/prompt-editor/ConfigEditorPane.tsx @@ -1,7 +1,10 @@ import React, { useState, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; import { colors } from '@/app/lib/colors'; import { ConfigBlob, Tool } from '@/app/configurations/prompt-editor/types'; import { SavedConfig, formatRelativeTime } from '@/app/lib/useConfigs'; +import { Validator } from './ValidatorListPane'; + interface ConfigEditorPaneProps { configBlob: ConfigBlob; @@ -19,6 +22,8 @@ interface ConfigEditorPaneProps { // Collapse functionality collapsed?: boolean; onToggle?: () => void; + // Guardrails + savedValidators?: Validator[]; } // Group configs by name for nested dropdown @@ -64,7 +69,9 @@ export default function ConfigEditorPane({ isSaving = false, collapsed = false, onToggle, + savedValidators = [], }: ConfigEditorPaneProps) { + const router = useRouter(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [showTooltip, setShowTooltip] = useState(null); @@ -588,7 +595,7 @@ export default function ConfigEditorPane({ }} >
- File Search + File Search +
+ ) : ( + + )} + {/* Save Button */} + ))} + + + + ); +} \ No newline at end of file diff --git a/app/configurations/prompt-editor/page.tsx b/app/configurations/prompt-editor/page.tsx index 9d269b2..c11764a 100644 --- a/app/configurations/prompt-editor/page.tsx +++ b/app/configurations/prompt-editor/page.tsx @@ -8,7 +8,7 @@ */ "use client" -import React, { useState, useEffect, Suspense } from 'react'; +import React, { useState, useEffect, Suspense, useRef } from 'react'; import { useSearchParams } from 'next/navigation'; import Sidebar from '@/app/components/Sidebar'; import { colors } from '@/app/lib/colors'; @@ -37,6 +37,7 @@ function PromptEditorContent() { const urlVersion = searchParams.get('version'); const showHistory = searchParams.get('history') === 'true'; const isNewConfig = searchParams.get('new') === 'true'; + const urlValidators = searchParams.get('validators'); // Comma-separated validator_config_ids // Evaluation context to preserve (when coming from evaluations page) const urlDatasetId = searchParams.get('dataset'); @@ -83,6 +84,10 @@ function PromptEditorContent() { const [selectedVersion, setSelectedVersion] = useState(null); const [compareWith, setCompareWith] = useState(null); + // Guardrails validators state + const [savedValidators, setSavedValidators] = useState([]); + const isLoadingFromUrl = useRef(false); + // Get API key from localStorage const getApiKey = (): string | null => { try { @@ -138,7 +143,9 @@ function PromptEditorContent() { if (targetConfig) { // Load the config setCurrentContent(targetConfig.promptContent); - setCurrentConfigBlob({ + + // Parse config blob to extract guardrails if present + const loadedConfigBlob: any = { completion: { provider: targetConfig.provider as any, params: { @@ -148,7 +155,27 @@ function PromptEditorContent() { tools: targetConfig.tools || [], }, }, - }); + }; + + // Check if this config has guardrails in the blob + // Note: SavedConfig type needs to be checked for these fields + const savedConfig = targetConfig as any; + + if (savedConfig.input_guardrails || savedConfig.output_guardrails) { + console.log('[DEBUG] Found guardrails in saved config'); + // Store guardrails in the blob for later saving + if (savedConfig.input_guardrails) { + loadedConfigBlob.input_guardrails = savedConfig.input_guardrails; + } + if (savedConfig.output_guardrails) { + loadedConfigBlob.output_guardrails = savedConfig.output_guardrails; + } + } else { + console.log('[DEBUG] No guardrails found in saved config'); + } + + console.log('[DEBUG] Final loadedConfigBlob:', loadedConfigBlob); + setCurrentConfigBlob(loadedConfigBlob); setProvider(targetConfig.provider); setTemperature(targetConfig.temperature); setSelectedConfigId(targetConfig.id); @@ -165,6 +192,262 @@ function PromptEditorContent() { } }, [initialLoadComplete, savedConfigs, urlConfigId, urlVersion, showHistory, isNewConfig]); + // Load validators from config blob + useEffect(() => { + const loadValidators = async () => { + // Skip if we're currently loading from URL parameters + if (isLoadingFromUrl.current) { + console.log('[ValidatorLoad] Skipping - loading from URL'); + return; + } + + try { + // Check if the config blob has guardrails + const inputGuardrails = (currentConfigBlob as any).input_guardrails || []; + const outputGuardrails = (currentConfigBlob as any).output_guardrails || []; + + if (inputGuardrails.length === 0 && outputGuardrails.length === 0) { + setSavedValidators([]); + return; + } + + const apiKey = getApiKey(); + if (!apiKey) { + console.error('[ValidatorLoad] No API key found'); + setSavedValidators([]); + return; + } + + // Get organization_id and project_id + const verifyResponse = await fetch('/api/apikeys/verify', { + method: 'GET', + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + if (!verifyResponse.ok) { + console.error('[ValidatorLoad] Failed to verify API key'); + setSavedValidators([]); + return; + } + + const verifyData = await verifyResponse.json(); + const organizationId = verifyData.data?.organization_id; + const projectId = verifyData.data?.project_id; + + if (!organizationId || !projectId) { + console.error('[ValidatorLoad] Could not retrieve organization or project ID'); + setSavedValidators([]); + return; + } + + const queryParams = new URLSearchParams({ + organization_id: organizationId, + project_id: projectId, + }); + + // Fetch all validators from both input and output guardrails + const allGuardrails = [ + ...inputGuardrails.map((g: any) => ({ ...g, stage: 'input' })), + ...outputGuardrails.map((g: any) => ({ ...g, stage: 'output' })), + ]; + + const validatorPromises = allGuardrails.map(async (guardrail: any) => { + try { + const response = await fetch( + `/api/guardrails/validators/configs/${guardrail.validator_config_id}?${queryParams.toString()}` + ); + + if (response.ok) { + const data = await response.json(); + if (data.success && data.data) { + // Map backend response to validator format + return { + id: data.data.type, + name: data.data.type.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()), + description: 'Configured guardrail', + enabled: data.data.is_enabled !== undefined ? data.data.is_enabled : true, + validator_config_id: guardrail.validator_config_id, + config: { + stage: guardrail.stage, + type: data.data.type, + ...data.data, + } + }; + } + } + } catch (error) { + console.error(`[ValidatorLoad] Failed to fetch validator ${guardrail.validator_config_id}:`, error); + } + return null; + }); + + const fetchedValidators = await Promise.all(validatorPromises); + const validValidators = fetchedValidators.filter(v => v !== null); + console.log('[ValidatorLoad] Loaded validators from config blob:', validValidators.length, validValidators); + setSavedValidators(validValidators); + } catch (e) { + console.error('Failed to load validators:', e); + setSavedValidators([]); + } + }; + + loadValidators(); + }, [currentConfigBlob]); + + // Handle validators from URL query param (when returning from safety-guardrails page) + useEffect(() => { + const fetchValidatorsFromUrl = async () => { + // If urlValidators is null/undefined, don't do anything (not returning from guardrails page) + // If urlValidators is empty string, it means all validators were removed + if (urlValidators === null || urlValidators === undefined) { + isLoadingFromUrl.current = false; + return; + } + + // Set flag to prevent config blob effect from interfering + isLoadingFromUrl.current = true; + + // Handle empty validators (all removed) + if (urlValidators === '') { + setSavedValidators([]); + setCurrentConfigBlob(prev => { + const updated = { ...prev }; + delete (updated as any).input_guardrails; + delete (updated as any).output_guardrails; + return updated; + }); + console.log('[ValidatorURL] All validators removed from config'); + // Reset flag after a longer delay to ensure config blob updates are complete + setTimeout(() => { + isLoadingFromUrl.current = false; + console.log('[ValidatorURL] Reset loading flag'); + }, 500); + return; + } + + const apiKey = getApiKey(); + if (!apiKey) { + console.error('[ValidatorURL] No API key found'); + return; + } + + try { + // Get organization_id and project_id + const verifyResponse = await fetch('/api/apikeys/verify', { + method: 'GET', + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + if (!verifyResponse.ok) { + console.error('[ValidatorURL] Failed to verify API key'); + return; + } + + const verifyData = await verifyResponse.json(); + const organizationId = verifyData.data?.organization_id; + const projectId = verifyData.data?.project_id; + + if (!organizationId || !projectId) { + console.error('[ValidatorURL] Could not retrieve organization or project ID'); + return; + } + + // Parse validator IDs from URL + const validatorIds = urlValidators.split(',').filter(id => id.trim()); + + const queryParams = new URLSearchParams({ + organization_id: organizationId, + project_id: projectId, + }); + + // Fetch each validator from backend + const validatorPromises = validatorIds.map(async (validatorId) => { + try { + const response = await fetch( + `/api/guardrails/validators/configs/${validatorId}?${queryParams.toString()}` + ); + + if (response.ok) { + const data = await response.json(); + if (data.success && data.data) { + // Map backend response to validator format + return { + id: data.data.type, + name: data.data.type.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()), + description: 'Configured guardrail', + enabled: data.data.is_enabled !== undefined ? data.data.is_enabled : true, + validator_config_id: validatorId, + config: { + stage: data.data.stage || 'input', + type: data.data.type, + ...data.data, + } + }; + } + } + } catch (error) { + console.error(`[ValidatorURL] Failed to fetch validator ${validatorId}:`, error); + } + return null; + }); + + const fetchedValidators = await Promise.all(validatorPromises); + const validValidators = fetchedValidators.filter(v => v !== null); + + console.log('[ValidatorURL] Fetched validators from URL:', validValidators.length, validValidators); + + if (validValidators.length > 0) { + setSavedValidators(validValidators); + + // IMPORTANT: Update the currentConfigBlob with the new validators + // This ensures the changes are detected and can be saved as a new version + const inputGuardrails = validValidators + .filter(v => (v.config?.stage || 'input') === 'input' && v.validator_config_id) + .map(v => ({ validator_config_id: v.validator_config_id })); + + const outputGuardrails = validValidators + .filter(v => v.config?.stage === 'output' && v.validator_config_id) + .map(v => ({ validator_config_id: v.validator_config_id })); + + setCurrentConfigBlob(prev => ({ + ...prev, + ...(inputGuardrails.length > 0 ? { input_guardrails: inputGuardrails } : {}), + ...(outputGuardrails.length > 0 ? { output_guardrails: outputGuardrails } : {}), + })); + + console.log('[ValidatorURL] Updated config blob with validators from URL'); + } else { + // No validators in URL - remove guardrails from config blob + setSavedValidators([]); + setCurrentConfigBlob(prev => { + const updated = { ...prev }; + delete (updated as any).input_guardrails; + delete (updated as any).output_guardrails; + return updated; + }); + console.log('[ValidatorURL] Removed all guardrails from config blob'); + } + + // Reset flag after a longer delay to ensure config blob updates are complete + setTimeout(() => { + isLoadingFromUrl.current = false; + console.log('[ValidatorURL] Reset loading flag'); + }, 500); + } catch (error) { + console.error('[ValidatorURL] Error fetching validators from URL:', error); + isLoadingFromUrl.current = false; + } + }; + + fetchValidatorsFromUrl(); + }, [urlValidators]); + // Detect unsaved changes useEffect(() => { if (!selectedConfigId) { @@ -181,7 +464,9 @@ function PromptEditorContent() { // Compare current state with selected config const promptChanged = currentContent !== selectedConfig.promptContent; - const configChanged = hasConfigChanges(currentConfigBlob, { + + // Build comparison config including guardrails if present + const savedConfigForComparison: any = { completion: { provider: selectedConfig.provider as any, params: { @@ -191,10 +476,21 @@ function PromptEditorContent() { tools: selectedConfig.tools || [], }, }, - }); + }; + + // Include guardrails from saved config for proper comparison + const savedConfigAny = selectedConfig as any; + if (savedConfigAny.input_guardrails) { + savedConfigForComparison.input_guardrails = savedConfigAny.input_guardrails; + } + if (savedConfigAny.output_guardrails) { + savedConfigForComparison.output_guardrails = savedConfigAny.output_guardrails; + } + + const configChanged = hasConfigChanges(currentConfigBlob, savedConfigForComparison); setHasUnsavedChanges(promptChanged || configChanged); - }, [selectedConfigId, currentContent, currentConfigBlob, provider, temperature, tools, savedConfigs]); + }, [selectedConfigId, currentContent, currentConfigBlob, provider, temperature, tools, savedConfigs, savedValidators]); // Save current configuration const handleSaveConfig = async () => { @@ -229,6 +525,16 @@ function PromptEditorContent() { } }); + // IMPORTANT: Only include validators that are enabled (toggle is ON) + const inputGuardrails = savedValidators + .filter(v => (v.config?.stage || 'input') === 'input' && v.validator_config_id && v.enabled !== false) + .map(v => ({ validator_config_id: v.validator_config_id! })); + + const outputGuardrails = savedValidators + .filter(v => v.config?.stage === 'output' && v.validator_config_id && v.enabled !== false) + .map(v => ({ validator_config_id: v.validator_config_id! })); + + const configBlob: ConfigBlob = { completion: { provider: currentConfigBlob.completion.provider, @@ -244,6 +550,11 @@ function PromptEditorContent() { }), }, }, + + // IMPORTANT: Always send guardrails fields - use empty arrays if no validators + // This ensures the backend removes validators that were toggled off + input_guardrails: inputGuardrails.length > 0 ? inputGuardrails : [], + output_guardrails: outputGuardrails.length > 0 ? outputGuardrails : [], }; // Check if updating existing config (same name exists) @@ -336,7 +647,9 @@ function PromptEditorContent() { if (!config) return; setCurrentContent(config.promptContent); - setCurrentConfigBlob({ + + // Build config blob with guardrails if present + const loadedConfigBlob: any = { completion: { provider: config.provider as any, type: config.type, @@ -347,7 +660,18 @@ function PromptEditorContent() { tools: config.tools || [], }, }, - }); + }; + + // Include guardrails if present + const savedConfig = config as any; + if (savedConfig.input_guardrails) { + loadedConfigBlob.input_guardrails = savedConfig.input_guardrails; + } + if (savedConfig.output_guardrails) { + loadedConfigBlob.output_guardrails = savedConfig.output_guardrails; + } + + setCurrentConfigBlob(loadedConfigBlob); setProvider(config.provider); setTemperature(config.temperature); setSelectedConfigId(config.id); @@ -441,6 +765,7 @@ function PromptEditorContent() { isSaving={isSaving} collapsed={!showConfigPane} onToggle={() => setShowConfigPane(!showConfigPane)} + savedValidators={savedValidators} /> diff --git a/app/configurations/prompt-editor/types.ts b/app/configurations/prompt-editor/types.ts index 22d740d..e4cd1bb 100644 --- a/app/configurations/prompt-editor/types.ts +++ b/app/configurations/prompt-editor/types.ts @@ -44,6 +44,9 @@ export interface ConfigBlob { max_num_results?: number; }; }; + // Guardrails validators + input_guardrails?: Array<{ validator_config_id: string }>; + output_guardrails?: Array<{ validator_config_id: string }>; } export interface Config { diff --git a/app/configurations/safety-guardrails/page.tsx b/app/configurations/safety-guardrails/page.tsx new file mode 100644 index 0000000..97d06ef --- /dev/null +++ b/app/configurations/safety-guardrails/page.tsx @@ -0,0 +1,1465 @@ +"use client" + +import { useState, useEffect, useCallback } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { colors } from '@/app/lib/colors'; +import Sidebar from '@/app/components/Sidebar'; +import ValidatorListPane, { Validator, AVAILABLE_VALIDATORS } from '@/app/components/prompt-editor/ValidatorListPane'; +import { useToast } from '@/app/components/Toast'; + +export default function SafetyGuardrailsPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const configId = searchParams?.get('config_id'); + const versionParam = searchParams?.get('version'); + const toast = useToast(); + + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [selectedValidator, setSelectedValidator] = useState(null); + const [validatorConfig, setValidatorConfig] = useState({}); + const [savedValidators, setSavedValidators] = useState([]); + const [isEditMode, setIsEditMode] = useState(false); + const [editingValidatorIndex, setEditingValidatorIndex] = useState(null); + const [editingValidatorId, setEditingValidatorId] = useState(null); + const [banLists, setBanLists] = useState([]); + const [showCreateBanListModal, setShowCreateBanListModal] = useState(false); + const [newBanList, setNewBanList] = useState({ name: '', description: '', banned_words: '', domain: '', is_public: false }); + + // Combined fetch: get validators from config AND all org validators + const fetchAllValidators = useCallback(async () => { + try { + const stored = localStorage.getItem('kaapi_api_keys'); + if (!stored) { + console.log('[SafetyGuardrails] No API key found'); + return; + } + + const keys = JSON.parse(stored); + if (keys.length === 0 || !keys[0].key) { + console.log('[SafetyGuardrails] No valid API key found'); + return; + } + + const apiKey = keys[0].key; + + // Get organization_id and project_id + const verifyResponse = await fetch('/api/apikeys/verify', { + method: 'GET', + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + if (!verifyResponse.ok) { + console.error('[SafetyGuardrails] Failed to verify API key'); + return; + } + + const verifyData = await verifyResponse.json(); + const organizationId = verifyData.data?.organization_id; + const projectId = verifyData.data?.project_id; + + if (!organizationId || !projectId) { + console.error('[SafetyGuardrails] Could not retrieve organization or project ID'); + return; + } + + const queryParams = new URLSearchParams({ + organization_id: organizationId, + project_id: projectId, + }); + + // Step 1: Fetch ALL validator configs for this org/project + const allValidatorsResponse = await fetch(`/api/guardrails/validators/configs?${queryParams.toString()}`); + const allValidatorsData = await allValidatorsResponse.json(); + + const allValidatorConfigs: Validator[] = []; + if (allValidatorsData.success && allValidatorsData.data) { + for (const validatorConfig of allValidatorsData.data) { + const validatorType = validatorConfig.type; + const validatorInfo = AVAILABLE_VALIDATORS.find(v => + v.id === validatorType.replace('_', '-') || + v.id === 'ban-list' && validatorType === 'ban_list' || + v.id === 'detect-pii' && validatorType === 'pii_remover' || + v.id === 'lexical-slur-match' && validatorType === 'uli_slur_match' || + v.id === 'gender-assumption-bias' && validatorType === 'gender_assumption_bias' + ); + + if (validatorInfo) { + allValidatorConfigs.push({ + id: validatorInfo.id, + name: validatorInfo.name, + description: validatorInfo.description, + enabled: false, // Default to OFF + validator_config_id: validatorConfig.id, + config: validatorConfig, + }); + } + } + } + + console.log('[SafetyGuardrails] Fetched all validator configs:', allValidatorConfigs); + + // Step 2: If viewing a specific config, get validators from that config + let configValidatorIds = new Set(); + if (configId) { + const configResponse = await fetch(`/api/configs/${configId}/versions`, { + headers: { 'X-API-KEY': apiKey }, + }); + + if (configResponse.ok) { + const configData = await configResponse.json(); + if (configData.success && configData.data && configData.data.length > 0) { + // Get the specified version or latest version + let targetVersion: any; + if (versionParam) { + targetVersion = configData.data.find((v: any) => v.version === parseInt(versionParam)); + if (!targetVersion) { + console.warn('[SafetyGuardrails] Specified version not found, using latest'); + targetVersion = configData.data[0]; + } + } else { + targetVersion = configData.data[0]; + } + + // Fetch the full version details + const versionResponse = await fetch(`/api/configs/${configId}/versions/${targetVersion.version}`, { + headers: { 'X-API-KEY': apiKey }, + }); + + if (versionResponse.ok) { + const versionData = await versionResponse.json(); + if (versionData.success && versionData.data) { + const configBlob = versionData.data.config_blob; + const inputGuardrails = configBlob.input_guardrails || []; + const outputGuardrails = configBlob.output_guardrails || []; + + // Collect all validator_config_ids from this config + configValidatorIds = new Set([ + ...inputGuardrails.map((g: any) => g.validator_config_id), + ...outputGuardrails.map((g: any) => g.validator_config_id), + ]); + + console.log('[SafetyGuardrails] Config has these validators:', Array.from(configValidatorIds)); + } + } + } + } + } + + // Step 3: Merge - show all org validators, with toggles ON for ones in this config + const mergedValidators = allValidatorConfigs.map(validator => ({ + ...validator, + enabled: configValidatorIds.has(validator.validator_config_id!), + })); + + console.log('[SafetyGuardrails] Final merged validators:', mergedValidators); + setSavedValidators(mergedValidators); + } catch (error) { + console.error('[SafetyGuardrails] Error fetching validators:', error); + } + }, [configId, versionParam]); + + useEffect(() => { + fetchAllValidators(); + }, [fetchAllValidators]); + + // Fetch ban lists when ban-list validator is selected + useEffect(() => { + if ((selectedValidator === 'ban-list' || editingValidatorId === 'ban-list')) { + fetchBanLists(); + } + }, [selectedValidator, editingValidatorId]); + + const fetchBanLists = async () => { + try { + const stored = localStorage.getItem('kaapi_api_keys'); + if (!stored) return; + + const keys = JSON.parse(stored); + if (keys.length === 0 || !keys[0].key) return; + + const apiKey = keys[0].key; + + const response = await fetch('/api/guardrails/ban_lists', { + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + const data = await response.json(); + setBanLists(data.data || []); + } + } catch (error) { + console.error('Error fetching ban lists:', error); + } + }; + + const handleCreateBanList = async () => { + try { + const stored = localStorage.getItem('kaapi_api_keys'); + if (!stored) { + toast.error('No API key found. Please add an API key first.'); + return; + } + + const keys = JSON.parse(stored); + if (keys.length === 0 || !keys[0].key) { + toast.error('No API key found. Please add an API key first.'); + return; + } + + const apiKey = keys[0].key; + + const bannedWordsArray = newBanList.banned_words + .split(/[\n,]+/) + .map(word => word.trim()) + .filter(word => word.length > 0); + + const payload = { + name: newBanList.name, + description: newBanList.description, + banned_words: bannedWordsArray, + domain: newBanList.domain, + is_public: newBanList.is_public, + }; + + const response = await fetch('/api/guardrails/ban_lists', { + method: 'POST', + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (response.ok) { + const data = await response.json(); + toast.success(`Ban list "${newBanList.name}" created successfully!`); + await fetchBanLists(); + if (data.data && data.data.id) { + setValidatorConfig({ ...validatorConfig, ban_list_id: data.data.id }); + } + setNewBanList({ name: '', description: '', banned_words: '', domain: '', is_public: false }); + setShowCreateBanListModal(false); + } else { + const errorData = await response.json(); + toast.error(`Failed to create ban list: ${errorData.error || 'Unknown error'}`); + } + } catch (error) { + console.error('Error creating ban list:', error); + toast.error('Failed to create ban list. Please try again.'); + } + }; + + const handleSaveValidator = async (validator: Validator) => { + try { + const stored = localStorage.getItem('kaapi_api_keys'); + if (!stored) { + toast.error('No API key found. Please add an API key first.'); + return; + } + + const keys = JSON.parse(stored); + if (keys.length === 0 || !keys[0].key) { + toast.error('No API key found. Please add an API key first.'); + return; + } + + const apiKey = keys[0].key; + + // First, verify the API key to get organization_id and project_id + const verifyResponse = await fetch('/api/apikeys/verify', { + method: 'GET', + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + if (!verifyResponse.ok) { + toast.error('Failed to verify API key.'); + return; + } + + const verifyData = await verifyResponse.json(); + const organizationId = verifyData.data?.organization_id; + const projectId = verifyData.data?.project_id; + + if (!organizationId || !projectId) { + toast.error('Could not retrieve organization or project ID.'); + return; + } + + // Build the request body with is_enabled field + const requestBody = { + type: validator.config.type, + stage: validator.config.stage, + is_enabled: validator.enabled !== undefined ? validator.enabled : true, + ...validator.config, + }; + + // Create validator config via API with query parameters + const queryParams = new URLSearchParams({ + organization_id: organizationId, + project_id: projectId, + }); + + const response = await fetch(`/api/guardrails/validators/configs?${queryParams.toString()}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorData = await response.json(); + toast.error(`Failed to save validator: ${errorData.error || 'Unknown error'}`); + return; + } + + const data = await response.json(); + + if (data.success && data.data && data.data.id) { + // Add validator_config_id to the validator + const validatorWithId = { + ...validator, + validator_config_id: data.data.id, + }; + + setSavedValidators([...savedValidators, validatorWithId]); + setSelectedValidator(null); + } else { + toast.error('Failed to save validator configuration.'); + } + } catch (error) { + console.error('Error saving validator:', error); + toast.error('Failed to save validator. Please try again.'); + } + }; + + const handleRemoveValidator = async (index: number) => { + const validator = savedValidators[index]; + if (!validator || !validator.validator_config_id) { + // If no validator_config_id, just remove from local state + setSavedValidators(savedValidators.filter((_, i) => i !== index)); + return; + } + + try { + const stored = localStorage.getItem('kaapi_api_keys'); + if (!stored) { + toast.error('No API key found. Please add an API key first.'); + return; + } + + const keys = JSON.parse(stored); + if (keys.length === 0 || !keys[0].key) { + toast.error('No API key found. Please add an API key first.'); + return; + } + + const apiKey = keys[0].key; + + // Get organization_id and project_id + const verifyResponse = await fetch('/api/apikeys/verify', { + method: 'GET', + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + if (!verifyResponse.ok) { + toast.error('Failed to verify API key.'); + return; + } + + const verifyData = await verifyResponse.json(); + const organizationId = verifyData.data?.organization_id; + const projectId = verifyData.data?.project_id; + + if (!organizationId || !projectId) { + toast.error('Could not retrieve organization or project ID.'); + return; + } + + const queryParams = new URLSearchParams({ + organization_id: organizationId, + project_id: projectId, + }); + + // Delete validator config via API + const response = await fetch( + `/api/guardrails/validators/configs/${validator.validator_config_id}?${queryParams.toString()}`, + { + method: 'DELETE', + } + ); + + if (!response.ok) { + const errorData = await response.json(); + toast.error(`Failed to delete validator: ${errorData.error || 'Unknown error'}`); + return; + } + + // Remove from local state after successful deletion + const updatedValidators = savedValidators.filter((_, i) => i !== index); + setSavedValidators(updatedValidators); + + toast.success(`Validator "${validator.name}" deleted successfully! Click "Save Configuration" in the main editor to persist changes.`); + } catch (error) { + console.error('Error deleting validator:', error); + toast.error('Failed to delete validator. Please try again.'); + } + }; + + const handleToggleValidator = (index: number) => { + const updatedValidators = [...savedValidators]; + const newEnabledState = !updatedValidators[index].enabled; + updatedValidators[index] = { + ...updatedValidators[index], + enabled: newEnabledState, + }; + console.log('[SafetyGuardrails] Toggled validator:', { + name: updatedValidators[index].name, + enabled: newEnabledState, + validator_config_id: updatedValidators[index].validator_config_id + }); + setSavedValidators(updatedValidators); + }; + + const handleEditValidator = (index: number) => { + const validator = savedValidators[index]; + setEditingValidatorIndex(index); + setEditingValidatorId(validator.id); + setValidatorConfig(validator.config); + setSelectedValidator(null); + setIsEditMode(false); // Exit edit mode when viewing a validator + }; + + const handleGoBack = () => { + // Build URL with validator IDs to pass back to main editor + // Only include validators that are enabled (toggle is ON) + const enabledValidators = savedValidators.filter(v => v.validator_config_id && v.enabled !== false); + const validatorIds = enabledValidators.map(v => v.validator_config_id).join(','); + + console.log('[SafetyGuardrails] Going back with validators:', { + total: savedValidators.length, + enabled: enabledValidators.length, + validators: enabledValidators, + ids: validatorIds + }); + + // Always include &validators param to signal we're returning from guardrails page + // Even if empty, this tells the main editor to update the config blob + const url = configId + ? `/configurations/prompt-editor?config=${configId}&validators=${validatorIds}` + : `/configurations/prompt-editor?validators=${validatorIds}`; + + router.push(url); + }; + + return ( +
+
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Title Section */} +
+
+ +
+

Safety Guardrails

+

Configure validators for your AI configuration

+
+ +
+
+ + {/* Content Area - Three Column Layout */} +
+ {/* Left: Validator List */} +
+ +
+ + {/* Middle: Configuration Panel */} +
+ {/* Header */} +
+

+ {(selectedValidator || editingValidatorId) + ? `${AVAILABLE_VALIDATORS.find(v => v.id === (editingValidatorId || selectedValidator))?.name} Configuration` + : 'Validator Configuration' + } +

+
+ + {/* Content */} +
+ {/* Validator Configuration Form */} + {(selectedValidator || editingValidatorId) && ( +
+
+ {/* Detect PII Validator */} + {(editingValidatorId || selectedValidator) === 'detect-pii' && ( + <> +
+ + +
+
+ + +
+
+ +
+
+ +
+ {['PERSON', 'PHONE_NUMBER', 'IN_AADHAAR', 'EMAIL_ADDRESS', 'CREDIT_CARD', 'IP_ADDRESS'].map((entity) => ( + + ))} +
+
+
+ + setValidatorConfig({ ...validatorConfig, threshold: parseFloat(e.target.value) })} + className="w-full" + style={{ accentColor: colors.accent.primary }} + /> +
+ + )} + + {/* Lexical Slur Match Validator */} + {(editingValidatorId || selectedValidator) === 'lexical-slur-match' && ( + <> +
+ + +
+
+ + +
+
+ +
+
+ +
+ {['en', 'hi'].map((lang) => ( + + ))} +
+
+
+ + setValidatorConfig({ ...validatorConfig, severity: e.target.value })} + className="w-full px-3 py-2 rounded-md text-sm focus:outline-none" + style={{ + border: `1px solid ${colors.border}`, + backgroundColor: colors.bg.primary, + color: colors.text.primary, + }} + /> +
+ + )} + + {/* Gender Assumption Bias Validator */} + {(editingValidatorId || selectedValidator) === 'gender-assumption-bias' && ( + <> +
+ + +
+
+ + +
+
+ +
+
+ + +
+ + )} + + {/* Ban List Validator */} + {(editingValidatorId || selectedValidator) === 'ban-list' && ( + <> +
+ + +
+
+ +
+
+ + +
+
+ + + + {/* Display selected ban list details */} + {validatorConfig.ban_list_id && (() => { + const selectedBanList = banLists.find(list => list.id === validatorConfig.ban_list_id); + return selectedBanList && ( +
+
+ + Banned Words ({selectedBanList.banned_words?.length || 0}) + + {selectedBanList.domain && ( + + {selectedBanList.domain} + + )} +
+ {selectedBanList.description && ( +

+ {selectedBanList.description} +

+ )} +
+ {selectedBanList.banned_words?.map((word: string, idx: number) => ( + + {word} + + ))} +
+
+ ); + })()} +
+ + )} + + {selectedValidator && ( + + )} +
+
+ )} + + {/* No Validator Selected - Empty State */} + {!selectedValidator && !editingValidatorId && ( +
+ + + +

+ Select any validator from the list +

+

+ Choose a validator to configure its settings +

+
+ )} +
+
+ + {/* Right: Configured Validators Panel */} +
+ {/* Header - Always visible */} +
+
+

+ Configured Validators {savedValidators.length > 0 && `(${savedValidators.length})`} +

+ {savedValidators.length > 0 && !isEditMode && ( + + )} +
+
+ + {/* Validators List Content */} +
+ {savedValidators.length > 0 ? ( +
+ + {/* Input Validators */} + {(() => { + const inputValidators = savedValidators.filter(v => (v.config?.stage || 'input') === 'input'); + return inputValidators.length > 0 && ( +
+

+ Input Validators +

+
+ {inputValidators.map((validator) => { + const originalIdx = savedValidators.indexOf(validator); + const isSelected = editingValidatorIndex === originalIdx; + return ( +
!isEditMode && handleEditValidator(originalIdx)} + title={!isEditMode ? "Click to view configuration" : ""} + > +
+
+
+ {validator.name} +
+ +
+ {isEditMode && ( +
e.stopPropagation()}> + +
+ )} +
+
+ ); + })} +
+
+ ); + })()} + + {/* Output Validators */} + {(() => { + const outputValidators = savedValidators.filter(v => v.config?.stage === 'output'); + return outputValidators.length > 0 && ( +
+

+ Output Validators +

+
+ {outputValidators.map((validator) => { + const originalIdx = savedValidators.indexOf(validator); + const isSelected = editingValidatorIndex === originalIdx; + return ( +
!isEditMode && handleEditValidator(originalIdx)} + title={!isEditMode ? "Click to view configuration" : ""} + > +
+
+
+ {validator.name} +
+ +
+ {isEditMode && ( +
e.stopPropagation()}> + +
+ )} +
+
+ ); + })} +
+
+ ); + })()} +
+ ) : ( +
+ + + +

+ No validators configured +

+

+ Configure a validator to see it here +

+
+ )} +
+
+
+
+
+ + {/* Create Ban List Modal */} + {showCreateBanListModal && ( + <> +
{ + setShowCreateBanListModal(false); + setNewBanList({ name: '', description: '', banned_words: '', domain: '', is_public: false }); + }} + /> +
+

+ Create New Ban List +

+ +
+
+ + setNewBanList({ ...newBanList, name: e.target.value })} + placeholder="e.g., profanity-filter" + className="w-full px-3 py-2 rounded-md text-sm focus:outline-none" + style={{ + border: `1px solid ${colors.border}`, + backgroundColor: colors.bg.primary, + color: colors.text.primary, + }} + /> +
+ +
+ + setNewBanList({ ...newBanList, description: e.target.value })} + placeholder="e.g., Filter for offensive and inappropriate language" + className="w-full px-3 py-2 rounded-md text-sm focus:outline-none" + style={{ + border: `1px solid ${colors.border}`, + backgroundColor: colors.bg.primary, + color: colors.text.primary, + }} + /> +
+ +
+ +