"use client"; import React, { useCallback, useMemo, useState, useRef, useEffect } from "react"; import Link from "next/link"; import { AdminRulesPanel } from "@/components/admin-rules-panel"; import { RuleExplanation } from "@/components/rule-explanation"; import { Footer } from "@/components/footer"; import { useTenant } from "@/contexts/TenantContext"; import { TenantSelector } from "@/components/tenant-selector"; import { canManageRules } from "@/lib/permissions"; const BACKEND_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_BASE_URL ?? "http://localhost:8000"; type StatusState = { tone: "info" | "success" | "error"; message: string } | null; const RBAC_ERROR_HINT = "Insufficient permissions for this action. Switch your role to Admin or Owner in the navbar and try again."; async function buildErrorMessage(response: Response) { const fallback = `Backend error ${response.status}`; try { const text = await response.text(); if (!text) { return response.status === 403 ? RBAC_ERROR_HINT : fallback; } try { const parsed = JSON.parse(text); const detail = parsed.detail || parsed.message; if (response.status === 403) { return detail || RBAC_ERROR_HINT; } return detail || fallback; } catch { if (response.status === 403) { return text || RBAC_ERROR_HINT; } return text || fallback; } } catch { return response.status === 403 ? RBAC_ERROR_HINT : fallback; } } export default function AdminRulesPage() { const { tenantId, role } = useTenant(); const [rulesInput, setRulesInput] = useState(""); const [deleteInput, setDeleteInput] = useState(""); const [rules, setRules] = useState([]); const [loading, setLoading] = useState(false); const [status, setStatus] = useState(null); const [isDragging, setIsDragging] = useState(false); const [lastUpdated, setLastUpdated] = useState(""); const [ruleExplanations, setRuleExplanations] = useState>({}); const [expandedRules, setExpandedRules] = useState>(new Set()); const [loadingExplanations, setLoadingExplanations] = useState>(new Set()); const fileInputRef = useRef(null); // Set initial time only on client side to avoid hydration mismatch useEffect(() => { setLastUpdated(new Date().toLocaleTimeString()); }, []); const headers = useMemo(() => { if (!tenantId.trim()) return undefined; return { "Content-Type": "application/json", "x-tenant-id": tenantId.trim(), "x-user-role": role, }; }, [tenantId, role]); const requireTenant = useCallback(() => { if (!tenantId.trim()) { setStatus({ tone: "error", message: "Enter a tenant ID in the navbar first." }); return false; } return true; }, [tenantId]); const handleRefresh = useCallback(async () => { if (!requireTenant()) return; try { setLoading(true); setStatus({ tone: "info", message: "Loading rules..." }); const response = await fetch(`${BACKEND_BASE_URL}/admin/rules`, { method: "GET", headers, }); if (!response.ok) { throw new Error(await buildErrorMessage(response)); } const data = await response.json(); setRules(data.rules ?? []); setLastUpdated(new Date().toLocaleTimeString()); setStatus({ tone: "success", message: "Rules synced." }); } catch (error: any) { setStatus({ tone: "error", message: error.message || "Failed to fetch rules" }); } finally { setLoading(false); } }, [headers, requireTenant]); const handleUpload = useCallback(async () => { if (!requireTenant()) return; const lines = rulesInput .split("\n") .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#")); // Filter out comments and empty lines if (!lines.length) { setStatus({ tone: "error", message: "Add at least one rule to upload. (Comment lines starting with # are ignored)" }); return; } try { setLoading(true); setStatus({ tone: "info", message: `Uploading ${lines.length} rule(s)...` }); const response = await fetch(`${BACKEND_BASE_URL}/admin/rules/bulk?enhance=true`, { method: "POST", headers, body: JSON.stringify({ rules: lines }), }); if (!response.ok) { throw new Error(await buildErrorMessage(response)); } const data = await response.json(); await handleRefresh(); setRulesInput(""); // Store explanations for display and auto-expand if (data.explanations && Array.isArray(data.explanations)) { const explanationsMap: Record = {}; const newExpanded = new Set(expandedRules); data.explanations.forEach((exp: any) => { if (exp.rule) { explanationsMap[exp.rule] = exp; // Auto-expand rules that have explanations if (exp.explanation || exp.examples || exp.missing_patterns) { newExpanded.add(exp.rule); } } }); setRuleExplanations((prev) => ({ ...prev, ...explanationsMap })); setExpandedRules(newExpanded); } const enhancedMsg = data.enhanced ? " (enhanced by LLM)" : ""; setStatus({ tone: "success", message: `Uploaded ${data.added_rules?.length || lines.length} rule(s)${enhancedMsg}.` }); } catch (error: any) { setStatus({ tone: "error", message: error.message || "Failed to upload rules" }); } finally { setLoading(false); } }, [handleRefresh, headers, requireTenant, rulesInput]); const processFile = useCallback(async (file: File) => { if (!requireTenant()) return; const fileExt = file.name.split('.').pop()?.toLowerCase(); if (!fileExt || !['txt', 'pdf', 'doc', 'docx', 'md'].includes(fileExt)) { setStatus({ tone: "error", message: "Unsupported file type. Supported: TXT, PDF, DOC, DOCX, MD" }); return; } try { setLoading(true); setStatus({ tone: "info", message: `Uploading and processing ${file.name}...` }); // For TXT files, read client-side for faster processing if (fileExt === 'txt' || fileExt === 'md') { const fileContent = await file.text(); const lines = fileContent .split("\n") .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#")); if (!lines.length) { setStatus({ tone: "error", message: "No valid rules found in file (after filtering comments)." }); setLoading(false); return; } // Upload rules via bulk endpoint setStatus({ tone: "info", message: `Uploading ${lines.length} rule(s)...` }); const response = await fetch(`${BACKEND_BASE_URL}/admin/rules/bulk?enhance=true`, { method: "POST", headers, body: JSON.stringify({ rules: lines }), }); if (!response.ok) { throw new Error(await buildErrorMessage(response)); } const data = await response.json(); await handleRefresh(); // Store explanations for display and auto-expand if (data.explanations && Array.isArray(data.explanations)) { const explanationsMap: Record = {}; const newExpanded = new Set(expandedRules); data.explanations.forEach((exp: any) => { if (exp.rule) { explanationsMap[exp.rule] = exp; // Auto-expand rules that have explanations if (exp.explanation || exp.examples || exp.missing_patterns) { newExpanded.add(exp.rule); } } }); setRuleExplanations((prev) => ({ ...prev, ...explanationsMap })); setExpandedRules(newExpanded); } const enhancedMsg = data.enhanced ? " (enhanced by LLM)" : ""; setStatus({ tone: "success", message: `Uploaded ${data.added_rules?.length || lines.length} rule(s) from ${file.name}${enhancedMsg}.` }); return; } // For PDF, DOC, DOCX - use backend file upload endpoint const formData = new FormData(); formData.append('file', file); setStatus({ tone: "info", message: `Extracting text from ${file.name}...` }); const response = await fetch(`${BACKEND_BASE_URL}/admin/rules/upload-file?enhance=true`, { method: "POST", headers: { "x-tenant-id": tenantId.trim(), "x-user-role": role, }, body: formData, }); if (!response.ok) { throw new Error(await buildErrorMessage(response)); } const data = await response.json(); await handleRefresh(); // Store explanations for display and auto-expand if (data.explanations && Array.isArray(data.explanations)) { const explanationsMap: Record = {}; const newExpanded = new Set(expandedRules); data.explanations.forEach((exp: any) => { if (exp.rule) { explanationsMap[exp.rule] = exp; // Auto-expand rules that have explanations if (exp.explanation || exp.examples || exp.missing_patterns) { newExpanded.add(exp.rule); } } }); setRuleExplanations((prev) => ({ ...prev, ...explanationsMap })); setExpandedRules(newExpanded); } const enhancedMsg = data.enhanced ? " (enhanced by LLM)" : ""; setStatus({ tone: "success", message: `Uploaded ${data.added_rules?.length || data.total_extracted || 0} rule(s) from ${file.name}${enhancedMsg}.` }); } catch (error: any) { setStatus({ tone: "error", message: error.message || "Failed to upload rules from file" }); } finally { setLoading(false); } }, [handleRefresh, headers, requireTenant, tenantId]); const handleFileUpload = useCallback(async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; await processFile(file); if (fileInputRef.current) { fileInputRef.current.value = ""; } }, [processFile]); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }, []); const handleDrop = useCallback(async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const file = e.dataTransfer.files?.[0]; if (!file) return; const fileExt = file.name.split('.').pop()?.toLowerCase(); if (!fileExt || !['txt', 'pdf', 'doc', 'docx', 'md'].includes(fileExt)) { setStatus({ tone: "error", message: "Unsupported file type. Supported: TXT, PDF, DOC, DOCX, MD" }); return; } await processFile(file); }, [processFile]); const fetchRuleExplanation = useCallback(async (rule: string) => { if (!requireTenant()) return; if (ruleExplanations[rule]) return; // Already have explanation try { setLoadingExplanations((prev) => new Set(prev).add(rule)); // Fetch explanation by calling the enhance endpoint // We'll use POST with the rule in the body to get explanation const response = await fetch( `${BACKEND_BASE_URL}/admin/rules?enhance=true`, { method: "POST", headers: { "Content-Type": "application/json", "x-tenant-id": tenantId.trim(), "x-user-role": role, }, body: JSON.stringify({ rule }), } ); if (response.ok) { const data = await response.json(); if (data.explanation || data.examples || data.missing_patterns) { setRuleExplanations((prev) => ({ ...prev, [rule]: { explanation: data.explanation, examples: data.examples || [], missing_patterns: data.missing_patterns || [], edge_cases: data.edge_cases || [], improvements: data.improvements || [], severity: data.severity, }, })); } } } catch (error) { console.error("Failed to fetch rule explanation:", error); } finally { setLoadingExplanations((prev) => { const next = new Set(prev); next.delete(rule); return next; }); } }, [tenantId, role, ruleExplanations, requireTenant]); const toggleRuleExplanation = useCallback((rule: string) => { setExpandedRules((prev) => { const next = new Set(prev); if (next.has(rule)) { next.delete(rule); } else { next.add(rule); // Fetch explanation if we don't have it if (!ruleExplanations[rule]) { fetchRuleExplanation(rule); } } return next; }); }, [ruleExplanations, fetchRuleExplanation]); const handleDelete = useCallback(async () => { if (!requireTenant()) return; if (!deleteInput.trim()) { setStatus({ tone: "error", message: "Enter the rule text you want to delete." }); return; } try { setLoading(true); setStatus({ tone: "info", message: "Deleting rule..." }); const response = await fetch( `${BACKEND_BASE_URL}/admin/rules/${encodeURIComponent(deleteInput.trim())}`, { method: "DELETE", headers, } ); if (!response.ok) { throw new Error(await buildErrorMessage(response)); } await handleRefresh(); setDeleteInput(""); setStatus({ tone: "success", message: "Rule deleted." }); } catch (error: any) { setStatus({ tone: "error", message: error.message || "Failed to delete rule" }); } finally { setLoading(false); } }, [deleteInput, handleRefresh, headers, requireTenant]); // Check permissions AFTER all hooks are called if (!canManageRules(role)) { return (
IC IntegraChat · Admin Rules
← Back Home

Access Denied

You need Admin or Owner role to manage rules.

Your current role: {role.charAt(0).toUpperCase() + role.slice(1)}

Please switch your role using the dropdown in the header.

); } return (
IC IntegraChat · Admin Rule Ingestion
← Back Home

Upload governance policies, compliance workflows, and red-flag patterns. Rules are automatically enhanced by LLM and stored in the backend.

LLM Enhanced 📄 File Upload 🔄 Chunk Processing
{lastUpdated && (
🔄 Last updated: {lastUpdated}
)} {!lastUpdated &&
}
{/* Left Column: Upload Rules */}
📝

Add Rules