"use client"; import { useCallback, useMemo, useState, useRef, useEffect } from "react"; import Link from "next/link"; import { AdminRulesPanel } from "@/components/admin-rules-panel"; import { Footer } from "@/components/footer"; import { useTenant } from "@/contexts/TenantContext"; import { TenantSelector } from "@/components/tenant-selector"; const BACKEND_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_BASE_URL ?? "http://localhost:8000"; type StatusState = { tone: "info" | "success" | "error"; message: string } | null; export default function AdminRulesPage() { const { tenantId } = 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 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(), }; }, [tenantId]); 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(`Backend error ${response.status}`); } 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) { const details = await response.text(); throw new Error(details || `Backend error ${response.status}`); } const data = await response.json(); await handleRefresh(); setRulesInput(""); 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) { const details = await response.text(); throw new Error(details || `Backend error ${response.status}`); } const data = await response.json(); await handleRefresh(); 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(), }, body: formData, }); if (!response.ok) { const details = await response.text(); throw new Error(details || `Backend error ${response.status}`); } const data = await response.json(); await handleRefresh(); 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 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) { const details = await response.text(); throw new Error(details || `Backend error ${response.status}`); } 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]); 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