<!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>