Notarial Compliance Checker

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Notarial Compliance Checker</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
        body {
            font-family: 'Inter', sans-serif;
            background-color: #f4f7f9;
        }
        .scrollable-list {
            max-height: 200px;
            overflow-y: auto;
        }
        .rule-card {
            transition: all 0.2s ease-in-out;
        }
        /* Custom scrollbar for aesthetics */
        .scrollable-list::-webkit-scrollbar {
            width: 8px;
        }
        .scrollable-list::-webkit-scrollbar-thumb {
            background-color: #cbd5e1; /* slate-300 */
            border-radius: 4px;
        }
    </style>
</head>
<body class="p-4 sm:p-6 md:p-8">

    <div id="loading-overlay" class="fixed inset-0 bg-gray-100 bg-opacity-75 z-50 flex items-center justify-center">
        <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
        <p class="ml-3 text-lg text-gray-700">Initializing App...</p>
    </div>

    <div id="app-container" class="max-w-4xl mx-auto opacity-0 transition-opacity duration-500">

        <header class="text-center mb-8 border-b-2 border-blue-200 pb-4">
            <h1 class="text-3xl font-extrabold text-blue-700">Notarial Certificate Compliance Tool</h1>
            <p class="text-gray-600 mt-1">Check a certificate's text against jurisdiction-specific or custom rules.</p>
            <p class="text-sm text-gray-500 mt-2">Current User ID: <span id="user-id-display" class="font-mono text-xs bg-gray-200 p-1 rounded">Loading...</span></p>
        </header>

        <!-- Input Area -->
        <div class="bg-white p-6 rounded-xl shadow-lg mb-8">
            <label for="certificate-text" class="block text-xl font-semibold mb-3 text-gray-700">1. Paste Notarial Certificate Text Here</label>
            <textarea id="certificate-text" rows="8" class="w-full p-4 border-2 border-blue-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-150 text-gray-800" placeholder="e.g., State of California, County of Los Angeles... Personally appeared John Doe..."></textarea>
            <button id="check-button" class="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-150 shadow-md hover:shadow-lg focus:outline-none focus:ring-4 focus:ring-blue-300">
                Check Compliance
            </button>
        </div>

        <!-- Compliance Rules and Setup -->
        <div class="grid md:grid-cols-2 gap-6 mb-8">
            <div class="bg-white p-6 rounded-xl shadow-lg">
                <h2 class="text-xl font-semibold text-gray-700 mb-4">2. Current Compliance Rules</h2>
                <div id="rules-list" class="scrollable-list border border-gray-200 rounded-lg p-3 space-y-2">
                    <!-- Rules will be dynamically inserted here -->
                    <p class="text-center text-gray-500 italic">Loading default rules or custom rules...</p>
                </div>
                <button id="load-custom-rules-button" class="mt-4 w-full bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-lg transition duration-150 focus:outline-none focus:ring-4 focus:ring-green-300">
                    Load/Save Custom Rules
                </button>
            </div>

            <!-- Add/Edit Rule Form (Initially Hidden) -->
            <div id="custom-rule-form-container" class="bg-white p-6 rounded-xl shadow-lg hidden">
                <h2 class="text-xl font-semibold text-gray-700 mb-4">Add a Custom Rule</h2>
                <form id="add-rule-form" class="space-y-3">
                    <div>
                        <label for="rule-name" class="block text-sm font-medium text-gray-600">Rule Name</label>
                        <input type="text" id="rule-name" required class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
                    </div>
                    <div>
                        <label for="rule-keyword" class="block text-sm font-medium text-gray-600">Keyword/Phrase (Case Insensitive)</label>
                        <input type="text" id="rule-keyword" required class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
                    </div>
                    <div class="flex items-center space-x-4">
                        <label class="flex items-center">
                            <input type="checkbox" id="rule-required" checked class="form-checkbox text-blue-600 h-5 w-5 rounded">
                            <span class="ml-2 text-sm font-medium text-gray-700">Required Field</span>
                        </label>
                    </div>
                    <button type="submit" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-lg transition duration-150">
                        Add & Save Rule
                    </button>
                </form>
                <button id="hide-rule-form-button" class="mt-2 w-full text-sm text-gray-500 hover:text-gray-700 transition duration-150">
                    Hide Form
                </button>
            </div>
        </div>

        <!-- Results Area -->
        <div id="results-area" class="bg-white p-6 rounded-xl shadow-2xl border-t-4 border-blue-500 hidden">
            <h2 class="text-2xl font-bold text-gray-800 mb-4">3. Compliance Check Results</h2>
            <div id="compliance-results" class="space-y-3">
                <!-- Results will be dynamically inserted here -->
            </div>
        </div>

    </div>

    <script type="module">
        import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
        import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
        import { getFirestore, doc, getDoc, addDoc, setDoc, onSnapshot, collection, query, where, getDocs, deleteDoc } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
        import { setLogLevel } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";

        // Global Firebase and App variables
        const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
        const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;
        const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;

        let app, db, auth, userId = null;
        let complianceRules = [];

        const defaultRules = [
            { id: 'default-1', ruleName: "Venue (State/County)", keyword: "State of", required: true },
            { id: 'default-2', ruleName: "Date of Notarial Act", keyword: "this [0-31] day of [A-Za-z]+", required: true, isRegex: true },
            { id: 'default-3', ruleName: "Signer's Name", keyword: "Personally appeared", required: true },
            { id: 'default-4', ruleName: "Notary's Signature Line", keyword: "Notary Public", required: true },
            { id: 'default-5', ruleName: "Type of Act", keyword: "(acknowledged|sworn to|affirmed|proved)", required: true },
            { id: 'default-6', ruleName: "Commission Expiration", keyword: "My Commission Expires", required: false }
        ];

        // --- Utility Functions ---

        /**
         * Retries a function with exponential backoff on failure.
         * @param {function} fn - The function to execute.
         * @param {number} maxRetries - Maximum number of retries.
         * @param {number} delay - Initial delay in milliseconds.
         */
        const withRetry = async (fn, maxRetries = 3, delay = 1000) => {
            for (let i = 0; i <= maxRetries; i++) {
                try {
                    return await fn();
                } catch (error) {
                    if (i === maxRetries) throw error;
                    await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)));
                }
            }
        };

        // --- Core Application Logic ---

        const initializeFirebase = async () => {
            if (!firebaseConfig) {
                console.error("Firebase config not available.");
                document.getElementById('loading-overlay').innerHTML = `<div class="p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">Firebase Config Missing. Cannot save custom rules.</div>`;
                return;
            }

            try {
                // setLogLevel('Debug');
                app = initializeApp(firebaseConfig);
                db = getFirestore(app);
                auth = getAuth(app);

                // 1. Authenticate
                if (initialAuthToken) {
                    await withRetry(() => signInWithCustomToken(auth, initialAuthToken));
                } else {
                    await withRetry(() => signInAnonymously(auth));
                }

                // 2. Set up Auth Listener and determine userId
                await new Promise(resolve => {
                    const unsubscribe = onAuthStateChanged(auth, (user) => {
                        if (user) {
                            userId = user.uid;
                            console.log("User authenticated:", userId);
                            document.getElementById('user-id-display').textContent = userId;
                            setupEventListeners();
                            loadCustomRules(); // Load rules once authenticated
                            document.getElementById('loading-overlay').classList.add('hidden');
                            document.getElementById('app-container').classList.add('opacity-100');
                        } else {
                            // Should not happen with anonymous/custom token sign-in, but handle case
                            console.error("No user authenticated.");
                        }
                        unsubscribe(); // Unsubscribe after the first state change
                        resolve();
                    });
                });

            } catch (error) {
                console.error("Error initializing Firebase or signing in:", error);
                document.getElementById('loading-overlay').innerHTML = `<div class="p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">Initialization Error. Check console.</div>`;
            }
        };

        const getRulesCollectionRef = () => {
            if (!db || !userId) return null;
            const userDocPath = `/artifacts/${appId}/users/${userId}`;
            return collection(db, userDocPath, 'notary_rules');
        };

        const loadCustomRules = () => {
            const rulesRef = getRulesCollectionRef();
            if (!rulesRef) {
                console.warn("Firestore not ready. Using default rules only.");
                complianceRules = [...defaultRules];
                renderRulesList();
                return;
            }

            // Start real-time listener for rules
            onSnapshot(rulesRef, (snapshot) => {
                const customRules = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));

                // Filter out any default rules that might share the same keyword as a custom rule if needed,
                // but for simplicity, we just add custom rules to the list.
                const updatedRules = [...defaultRules.map(r => ({...r, isDefault: true}))];
                customRules.forEach(cr => {
                    const existingDefault = updatedRules.findIndex(r => r.keyword === cr.keyword && r.isDefault);
                    if(existingDefault > -1) {
                         // Replace or remove conflicting default rule
                         updatedRules.splice(existingDefault, 1);
                    }
                    updatedRules.push(cr);
                });

                complianceRules = updatedRules;
                renderRulesList();
            }, (error) => {
                console.error("Error listening to custom rules:", error);
                complianceRules = [...defaultRules];
                renderRulesList();
            });
        };

        const renderRulesList = () => {
            const listContainer = document.getElementById('rules-list');
            listContainer.innerHTML = '';

            if (complianceRules.length === 0) {
                listContainer.innerHTML = `<p class="text-center text-gray-500 italic">No rules defined. Add a custom rule above.</p>`;
                return;
            }

            complianceRules.forEach(rule => {
                const requiredBadge = rule.required ?
                    `<span class="text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full bg-red-100 text-red-800">MANDATORY</span>` :
                    `<span class="text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full bg-yellow-100 text-yellow-800">OPTIONAL</span>`;
                
                const typeBadge = rule.isRegex ?
                    `<span class="text-xs font-mono px-2.5 py-0.5 rounded bg-purple-100 text-purple-800">REGEX</span>` :
                    `<span class="text-xs font-mono px-2.5 py-0.5 rounded bg-teal-100 text-teal-800">PHRASE</span>`;
                
                const deleteButton = rule.isDefault ? '' :
                    `<button data-id="${rule.id}" class="delete-rule-button text-red-500 hover:text-red-700 ml-2" title="Delete Custom Rule">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
                    </button>`;

                const ruleDiv = document.createElement('div');
                ruleDiv.className = 'rule-card flex justify-between items-center bg-gray-50 p-3 rounded-lg border border-gray-200 hover:bg-gray-100';
                ruleDiv.innerHTML = `
                    <div class="flex-grow">
                        <p class="font-medium text-gray-800">${rule.ruleName} ${rule.isDefault ? '<span class="text-xs text-blue-500 italic">(Default)</span>' : ''}</p>
                        <p class="text-xs text-gray-500 truncate mt-0.5">Keyword: 
                            <span class="font-mono text-gray-600">${rule.keyword}</span>
                        </p>
                        <div class="mt-1">
                            ${requiredBadge}
                            ${typeBadge}
                        </div>
                    </div>
                    ${deleteButton}
                `;
                listContainer.appendChild(ruleDiv);
            });
            
            // Re-attach delete listeners
            document.querySelectorAll('.delete-rule-button').forEach(button => {
                button.addEventListener('click', handleDeleteRule);
            });
        };

        const handleAddRule = async (event) => {
            event.preventDefault();
            const rulesRef = getRulesCollectionRef();
            if (!rulesRef) {
                console.error("Firestore not initialized. Cannot save rule.");
                return;
            }

            const ruleName = document.getElementById('rule-name').value;
            const keyword = document.getElementById('rule-keyword').value;
            const required = document.getElementById('rule-required').checked;
            
            // Simple check to infer if it's a regex (contains common regex characters)
            const isRegex = /[\\.*?+^$[\](){}|-]/.test(keyword);

            const newRule = {
                ruleName,
                keyword,
                required,
                isRegex,
                createdAt: new Date().toISOString()
            };

            try {
                await withRetry(() => addDoc(rulesRef, newRule));
                document.getElementById('add-rule-form').reset();
            } catch (error) {
                console.error("Error adding rule:", error);
            }
        };

        const handleDeleteRule = async (event) => {
            const button = event.currentTarget;
            const ruleId = button.getAttribute('data-id');
            const rulesRef = getRulesCollectionRef();

            if (!rulesRef || !ruleId) {
                console.error("Cannot delete rule: Firestore not ready or ID missing.");
                return;
            }

            // Create a simple confirmation box (modal is better, but using basic UI text here)
            if (!confirm(`Are you sure you want to delete the rule with ID: ${ruleId}?`)) {
                return;
            }

            try {
                const docRef = doc(rulesRef, ruleId);
                await withRetry(() => deleteDoc(docRef));
                console.log(`Rule ${ruleId} deleted.`);
            } catch (error) {
                console.error("Error deleting rule:", error);
            }
        };

        const checkCompliance = () => {
            const certificateText = document.getElementById('certificate-text').value.toLowerCase();
            const resultsContainer = document.getElementById('compliance-results');
            resultsContainer.innerHTML = '';
            document.getElementById('results-area').classList.remove('hidden');

            let allRequiredPassed = true;

            complianceRules.forEach(rule => {
                let passed = false;
                let matchDetails = '';
                const baseClass = "p-3 rounded-lg shadow-sm flex justify-between items-start";
                let resultClass, iconSvg;

                try {
                    if (rule.isRegex) {
                        // Regex check
                        const regex = new RegExp(rule.keyword, 'gi');
                        const match = certificateText.match(regex);
                        if (match && match.length > 0) {
                            passed = true;
                            matchDetails = `Found ${match.length} match(es) for pattern: /${rule.keyword}/`;
                        } else {
                            matchDetails = `Pattern /${rule.keyword}/ not found.`;
                        }
                    } else {
                        // Simple phrase check
                        const keywordLower = rule.keyword.toLowerCase();
                        if (certificateText.includes(keywordLower)) {
                            passed = true;
                            matchDetails = `Keyword "${rule.keyword}" found.`;
                        } else {
                            matchDetails = `Keyword "${rule.keyword}" not found.`;
                        }
                    }
                } catch (e) {
                    console.error(`Error checking rule ${rule.ruleName}:`, e);
                    passed = false;
                    matchDetails = `ERROR during check. Check keyword format.`;
                }


                if (passed) {
                    resultClass = "bg-green-100 border-l-4 border-green-500";
                    iconSvg = `<svg class="w-6 h-6 text-green-600 mr-3 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>`;
                } else {
                    if (rule.required) {
                        allRequiredPassed = false;
                        resultClass = "bg-red-100 border-l-4 border-red-500";
                        iconSvg = `<svg class="w-6 h-6 text-red-600 mr-3 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>`;
                    } else {
                        resultClass = "bg-yellow-100 border-l-4 border-yellow-500";
                        iconSvg = `<svg class="w-6 h-6 text-yellow-600 mr-3 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>`;
                    }
                }

                const resultDiv = document.createElement('div');
                resultDiv.className = `${baseClass} ${resultClass}`;
                resultDiv.innerHTML = `
                    <div class="flex">
                        ${iconSvg}
                        <div>
                            <p class="font-semibold text-lg text-gray-900">${rule.ruleName}</p>
                            <p class="text-sm text-gray-700 mt-1">${matchDetails}</p>
                        </div>
                    </div>
                    <div class="text-right">
                        <span class="text-sm font-bold ${passed ? 'text-green-700' : 'text-red-700'}">${passed ? 'PASS' : 'FAIL'}</span>
                        <p class="text-xs ${rule.required ? 'text-red-600' : 'text-gray-500'}">${rule.required ? 'REQUIRED' : 'OPTIONAL'}</p>
                    </div>
                `;
                resultsContainer.appendChild(resultDiv);
            });

            // Overall Summary
            const summaryDiv = document.createElement('div');
            if (allRequiredPassed) {
                summaryDiv.className = "mt-6 p-4 bg-blue-50 border-l-4 border-blue-600 rounded-lg text-lg font-bold text-blue-800";
                summaryDiv.textContent = "OVERALL STATUS: Required elements are PRESENT. Review optional elements.";
            } else {
                summaryDiv.className = "mt-6 p-4 bg-red-50 border-l-4 border-red-600 rounded-lg text-lg font-bold text-red-800";
                summaryDiv.textContent = "OVERALL STATUS: FAILED. One or more REQUIRED elements are MISSING.";
            }
            resultsContainer.prepend(summaryDiv);
        };

        const setupEventListeners = () => {
            document.getElementById('check-button').addEventListener('click', checkCompliance);
            document.getElementById('add-rule-form').addEventListener('submit', handleAddRule);
            
            const formContainer = document.getElementById('custom-rule-form-container');
            document.getElementById('load-custom-rules-button').addEventListener('click', () => {
                formContainer.classList.toggle('hidden');
            });
            document.getElementById('hide-rule-form-button').addEventListener('click', (e) => {
                e.preventDefault();
                formContainer.classList.add('hidden');
            });
        };

        // Initialize the application on load
        initializeFirebase();

    </script>
</body>
</html>