diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..b7c310e5a4548c4822e136aff48b48ec823fe662 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+rights_navigator/database/chroma_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
diff --git a/Backend/README.md b/Backend/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d3f5a12faa99758192ecc4ed3fc22c9249232e86
--- /dev/null
+++ b/Backend/README.md
@@ -0,0 +1 @@
+
diff --git a/Backend/requirements.txt b/Backend/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..aa17f677349a29d3a648ccb8f56949eb3aecd0cf
--- /dev/null
+++ b/Backend/requirements.txt
@@ -0,0 +1,5 @@
+fastapi>=0.110.0
+uvicorn>=0.28.0
+python-multipart>=0.0.9
+gradio_client>=0.17.0
+pymupdf>=1.24.0
diff --git a/Backend/server.py b/Backend/server.py
new file mode 100644
index 0000000000000000000000000000000000000000..14116367c905ecebdefc9c3992dce71171d37e6b
--- /dev/null
+++ b/Backend/server.py
@@ -0,0 +1,345 @@
+import os
+import sys
+import tempfile
+from pathlib import Path
+
+from fastapi import FastAPI, File, HTTPException, UploadFile
+from fastapi.middleware.cors import CORSMiddleware
+from gradio_client import Client, handle_file
+from pydantic import BaseModel
+
+
+# ============================================================
+# PROJECT PATH
+# ============================================================
+
+PROJECT_ROOT = Path(__file__).resolve().parent.parent
+
+# Allow Python to find:
+# oosc/rights_navigator/
+sys.path.append(str(PROJECT_ROOT))
+
+
+# ============================================================
+# RIGHTS NAVIGATOR
+# ============================================================
+
+from rights_navigator.rights_navigator import get_rights_response
+
+
+# ============================================================
+# FASTAPI APP
+# ============================================================
+
+app = FastAPI(
+ title="Civic & RTI Hub API"
+)
+
+
+# ============================================================
+# CORS
+# ============================================================
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+# ============================================================
+# HUGGING FACE TOKEN
+# ============================================================
+
+HF_TOKEN = os.getenv("HF_TOKEN")
+
+
+# ============================================================
+# HUGGING FACE SPACES
+# ============================================================
+
+# Friend's PDF Simplifier
+SIMPLIFIER_SPACE_ID = "Ronak0/RC"
+
+# Friend's RTI Drafter
+DRAFTER_SPACE_ID = "Ronak0/rti-drafter"
+
+
+# ============================================================
+# CLIENTS
+# ============================================================
+
+# IMPORTANT:
+# Do NOT connect to the Spaces when the server starts.
+#
+# They will be initialized only when /simplify or /draft
+# is actually called.
+
+simplifier_client = None
+drafter_client = None
+
+
+# ============================================================
+# LAZY LOAD - PDF SIMPLIFIER
+# ============================================================
+
+def get_simplifier_client():
+
+ global simplifier_client
+
+ if simplifier_client is None:
+
+ print("Connecting to PDF Simplifier Space...")
+
+ simplifier_client = Client(
+ SIMPLIFIER_SPACE_ID,
+ token=HF_TOKEN
+ )
+
+ print("PDF Simplifier connected.")
+
+ return simplifier_client
+
+
+# ============================================================
+# LAZY LOAD - RTI DRAFTER
+# ============================================================
+
+def get_drafter_client():
+
+ global drafter_client
+
+ if drafter_client is None:
+
+ print("Connecting to RTI Drafter Space...")
+
+ drafter_client = Client(
+ DRAFTER_SPACE_ID,
+ token=HF_TOKEN
+ )
+
+ print("RTI Drafter connected.")
+
+ return drafter_client
+
+
+# ============================================================
+# REQUEST MODELS
+# ============================================================
+
+class DraftRequest(BaseModel):
+ prompt: str
+
+
+class RightsRequest(BaseModel):
+ grievance: str
+
+
+# ============================================================
+# HEALTH CHECK
+# ============================================================
+
+@app.get("/")
+async def root():
+
+ return {
+ "status": "online",
+ "service": "Civic & RTI Hub API",
+ "endpoints": [
+ "/simplify",
+ "/draft",
+ "/rights"
+ ]
+ }
+
+
+# ============================================================
+# PDF SIMPLIFIER
+# ============================================================
+
+@app.post("/simplify")
+@app.post("/api/simplify")
+async def simplify_pdf(
+ file: UploadFile = File(...)
+):
+
+ # --------------------------------------------------------
+ # Validate PDF
+ # --------------------------------------------------------
+
+ if not file.filename.lower().endswith(".pdf"):
+
+ raise HTTPException(
+ status_code=400,
+ detail="Must be a PDF file."
+ )
+
+
+ # --------------------------------------------------------
+ # Save uploaded PDF temporarily
+ # --------------------------------------------------------
+
+ with tempfile.NamedTemporaryFile(
+ delete=False,
+ suffix=".pdf"
+ ) as tmp:
+
+ tmp.write(
+ await file.read()
+ )
+
+ tmp_path = tmp.name
+
+
+ try:
+
+ # ----------------------------------------------------
+ # Connect only when this endpoint is used
+ # ----------------------------------------------------
+
+ client = get_simplifier_client()
+
+
+ # ----------------------------------------------------
+ # Call friend's Hugging Face Space
+ # ----------------------------------------------------
+
+ raw_result = client.predict(
+ pdf_file=handle_file(tmp_path),
+ api_name="/simplify_document"
+ )
+
+
+ # ----------------------------------------------------
+ # Format response
+ # ----------------------------------------------------
+
+ points = [
+ p.strip()
+ for p in raw_result.split("\n\n")
+ if p.strip()
+ ]
+
+
+ return {
+ "simplified_points": points
+ }
+
+
+ except Exception as e:
+
+ raise HTTPException(
+ status_code=500,
+ detail=f"PDF simplifier error: {str(e)}"
+ )
+
+
+ finally:
+
+ # ----------------------------------------------------
+ # Delete temporary PDF
+ # ----------------------------------------------------
+
+ if os.path.exists(tmp_path):
+
+ os.remove(tmp_path)
+
+
+# ============================================================
+# RTI DRAFTER
+# ============================================================
+
+@app.post("/draft")
+@app.post("/api/draft")
+async def draft_rti_endpoint(
+ request: DraftRequest
+):
+
+ # --------------------------------------------------------
+ # Validate request
+ # --------------------------------------------------------
+
+ if not request.prompt.strip():
+
+ raise HTTPException(
+ status_code=400,
+ detail="Prompt cannot be empty."
+ )
+
+
+ try:
+
+ # ----------------------------------------------------
+ # Connect only when this endpoint is used
+ # ----------------------------------------------------
+
+ client = get_drafter_client()
+
+
+ # ----------------------------------------------------
+ # Call friend's Hugging Face Space
+ # ----------------------------------------------------
+
+ draft_result = client.predict(
+ user_prompt=request.prompt,
+ api_name="/draft_rti"
+ )
+
+
+ return {
+ "draft": draft_result
+ }
+
+
+ except Exception as e:
+
+ raise HTTPException(
+ status_code=500,
+ detail=f"RTI drafter error: {str(e)}"
+ )
+
+
+# ============================================================
+# RIGHTS NAVIGATOR
+# ============================================================
+
+@app.post("/rights")
+@app.post("/api/rights")
+async def rights_navigator_endpoint(
+ request: RightsRequest
+):
+
+ # --------------------------------------------------------
+ # Validate grievance
+ # --------------------------------------------------------
+
+ if not request.grievance.strip():
+
+ raise HTTPException(
+ status_code=400,
+ detail="Grievance cannot be empty."
+ )
+
+
+ try:
+
+ # ----------------------------------------------------
+ # Call YOUR Rights Navigator
+ # ----------------------------------------------------
+
+ result = get_rights_response(
+ request.grievance
+ )
+
+
+ return result
+
+
+ except Exception as e:
+
+ raise HTTPException(
+ status_code=500,
+ detail=f"Rights Navigator error: {str(e)}"
+ )
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..fed93b8981f340d2a344a2361adbe6684f21abeb
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+FROM python:3.12-slim
+
+WORKDIR /app
+
+ENV PYTHONUNBUFFERED=1
+ENV PYTHONDONTWRITEBYTECODE=1
+
+RUN apt-get update && apt-get install -y \
+ git \
+ git-lfs \
+ curl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY requirements.txt .
+
+RUN pip install --no-cache-dir --upgrade pip
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+EXPOSE 7860
+
+CMD ["uvicorn", "Backend.server:app", "--host", "0.0.0.0", "--port", "7860"]
\ No newline at end of file
diff --git a/Frontend/README.md b/Frontend/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d3f5a12faa99758192ecc4ed3fc22c9249232e86
--- /dev/null
+++ b/Frontend/README.md
@@ -0,0 +1 @@
+
diff --git a/Frontend/app.js b/Frontend/app.js
new file mode 100644
index 0000000000000000000000000000000000000000..9b8c4a8ab48924ae8dbc1d9f7844afd17bfc106c
--- /dev/null
+++ b/Frontend/app.js
@@ -0,0 +1,104 @@
+// Ensure this matches your Render Web Service URL
+const BACKEND_BASE_URL = "https://caseloop.onrender.com";
+
+// --- Feature 1: PDF Simplifier Logic ---
+const submitBtn = document.getElementById("submitBtn");
+const statusText = document.getElementById("statusText");
+const resultsList = document.getElementById("resultsList");
+
+submitBtn.addEventListener("click", async () => {
+ const fileInput = document.getElementById("pdfInput").files[0];
+
+ if (!fileInput) {
+ statusText.innerText = "Error: Please select a PDF file first.";
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append("file", fileInput);
+
+ statusText.innerText = "Sending to Render Backend (Please wait)...";
+ resultsList.innerHTML = "";
+ submitBtn.disabled = true;
+
+ try {
+ const response = await fetch(`${BACKEND_BASE_URL}/simplify`, {
+ method: "POST",
+ body: formData
+ });
+
+ const rawText = await response.text();
+ console.log("Server HTTP Status:", response.status);
+ console.log("Raw Server Response:", rawText);
+
+ if (!response.ok) {
+ throw new Error(`Server returned HTTP ${response.status}: ${rawText.slice(0, 200)}`);
+ }
+
+ const data = JSON.parse(rawText);
+ statusText.innerText = "Success! Generated Points:";
+
+ if (data.simplified_points && Array.isArray(data.simplified_points)) {
+ data.simplified_points.forEach(point => {
+ const li = document.createElement("li");
+ li.textContent = point;
+ resultsList.appendChild(li);
+ });
+ } else {
+ statusText.innerText = "Received response, but no points found.";
+ }
+
+ } catch (error) {
+ console.error("Connection failed:", error);
+ statusText.innerText = "Error: " + error.message;
+ } finally {
+ submitBtn.disabled = false;
+ }
+});
+
+// --- Feature 2: RTI Drafter Logic ---
+const draftBtn = document.getElementById("draftBtn");
+const promptInput = document.getElementById("promptInput");
+const draftStatusText = document.getElementById("draftStatusText");
+const draftResultContainer = document.getElementById("draftResultContainer");
+const draftResult = document.getElementById("draftResult");
+
+draftBtn.addEventListener("click", async () => {
+ const prompt = promptInput.value.trim();
+
+ if (!prompt) {
+ draftStatusText.innerText = "Error: Please enter a query or problem description.";
+ return;
+ }
+
+ draftStatusText.innerText = "Generating RTI draft (Please wait)...";
+ draftResultContainer.style.display = "none";
+ draftResult.textContent = "";
+ draftBtn.disabled = true;
+
+ try {
+ const response = await fetch(`${BACKEND_BASE_URL}/draft`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ prompt: prompt })
+ });
+
+ const rawText = await response.text();
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${rawText.slice(0, 200)}`);
+ }
+
+ const data = JSON.parse(rawText);
+ draftStatusText.innerText = "Draft generated successfully!";
+ draftResult.textContent = data.draft;
+ draftResultContainer.style.display = "block";
+
+ } catch (error) {
+ console.error("Drafter error:", error);
+ draftStatusText.innerText = "Error: " + error.message;
+ } finally {
+ draftBtn.disabled = false;
+ }
+});
diff --git a/Frontend/index.html b/Frontend/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..db5aad8b5d0a71e56b1392dadc8cd06beb8833a3
--- /dev/null
+++ b/Frontend/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ CaseLoop — Civic Legal & RTI Intelligence Engine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Frontend/package.json b/Frontend/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..7aa7d9f5189006251388e586ba3ac5f5dbb84a5c
--- /dev/null
+++ b/Frontend/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "react-example",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3000 --host=0.0.0.0",
+ "build": "vite build",
+ "preview": "vite preview",
+ "start": "tsx server.ts",
+ "clean": "rm -rf dist server.js",
+ "lint": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@google/genai": "^2.4.0",
+ "@tailwindcss/vite": "^4.1.14",
+ "@vitejs/plugin-react": "^5.0.4",
+ "dotenv": "^17.2.3",
+ "express": "^4.21.2",
+ "firebase": "^12.18.0",
+ "jspdf": "^4.2.1",
+ "lucide-react": "^0.546.0",
+ "motion": "^12.23.24",
+ "react": "^19.0.1",
+ "react-dom": "^19.0.1",
+ "tsx": "^4.21.0",
+ "vite": "^6.2.3"
+ },
+ "devDependencies": {
+ "@firebase/eslint-plugin-security-rules": "^0.0.2",
+ "@types/express": "^4.17.21",
+ "@types/node": "^22.14.0",
+ "autoprefixer": "^10.4.21",
+ "esbuild": "^0.25.0",
+ "tailwindcss": "^4.1.14",
+ "typescript": "~5.8.2"
+ }
+}
diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..222e192966dfee3158e1eed035f2d7c07b6c99b9
--- /dev/null
+++ b/Frontend/src/App.tsx
@@ -0,0 +1,558 @@
+import React, { useState, useEffect } from 'react';
+import { Header } from './components/Header';
+import { Footer } from './components/Footer';
+import { RulebookSelector } from './components/RulebookSelector';
+import { FilingWorkspace } from './components/FilingWorkspace';
+import { MyFilingsDashboard } from './components/MyFilingsDashboard';
+import { RulebooksModal } from './components/RulebooksModal';
+import { AppealTrackerModal } from './components/AppealTrackerModal';
+import { RevisionHistoryModal } from './components/RevisionHistoryModal';
+import { ShareModal } from './components/ShareModal';
+import { PDFExportModal } from './components/PDFExportModal';
+import { LegalInfoModal } from './components/LegalInfoModal';
+import { GuidedIntakeModal } from './components/GuidedIntakeModal';
+import { RtiAuditorModal } from './components/RtiAuditorModal';
+import { INITIAL_CASES, INITIAL_RULEBOOKS } from './data/mockData';
+import { LegalCase, Rulebook, ExtractedFact } from './types';
+import { AIAnalysisResponse } from './services/api';
+import { useAuth } from './context/AuthContext';
+import {
+ subscribeToUserCases,
+ saveUserCaseToFirestore,
+ deleteUserCaseFromFirestore,
+} from './firebase';
+
+export default function App() {
+ const { user } = useAuth();
+ const [currentView, setCurrentView] = useState<'home' | 'workspace' | 'my-filings'>('home');
+ const [cases, setCases] = useState(INITIAL_CASES);
+ const [selectedCaseId, setSelectedCaseId] = useState(INITIAL_CASES[0].id);
+ const [rulebooks] = useState(INITIAL_RULEBOOKS);
+ const [selectedCity, setSelectedCity] = useState('Delhi (NCR)');
+ const [toastMessage, setToastMessage] = useState(null);
+
+ // Modals state
+ const [showRulebooksModal, setShowRulebooksModal] = useState(false);
+ const [showAppealTracker, setShowAppealTracker] = useState(false);
+ const [showRevisionHistory, setShowRevisionHistory] = useState(false);
+ const [showGuidedIntake, setShowGuidedIntake] = useState(false);
+ const [guidedIntakeCategory, setGuidedIntakeCategory] = useState(undefined);
+ const [showRtiAuditor, setShowRtiAuditor] = useState(false);
+ const [caseToShare, setCaseToShare] = useState(null);
+ const [caseToPDF, setCaseToPDF] = useState(null);
+ const [legalModalType, setLegalModalType] = useState<
+ 'disclaimer' | 'accessibility' | 'rulebooks' | 'privacy' | null
+ >(null);
+
+ const showToast = (msg: string) => {
+ setToastMessage(msg);
+ setTimeout(() => setToastMessage(null), 3000);
+ };
+
+ // Real-time Firestore synchronization when user is signed in
+ useEffect(() => {
+ if (!user) {
+ // If user logs out or is guest, retain in-memory state
+ return;
+ }
+
+ // Subscribe to Firestore cases subcollection
+ const unsubscribe = subscribeToUserCases(
+ user.uid,
+ (remoteCases) => {
+ setCases(remoteCases);
+ if (remoteCases.length > 0) {
+ if (!remoteCases.some((c) => c.id === selectedCaseId)) {
+ setSelectedCaseId(remoteCases[0].id);
+ }
+ }
+ },
+ (err) => {
+ console.error('Real-time sync listener error:', err);
+ }
+ );
+
+ return () => unsubscribe();
+ }, [user]);
+
+ const currentCase = cases.find((c) => c.id === selectedCaseId) || cases[0] || INITIAL_CASES[0];
+
+ const handleUpdateCase = (updatedCase: LegalCase) => {
+ setCases((prev) => prev.map((c) => (c.id === updatedCase.id ? updatedCase : c)));
+ if (user) {
+ saveUserCaseToFirestore(user.uid, updatedCase).catch((err) =>
+ console.error('Failed to sync updated case to Firestore:', err)
+ );
+ }
+ };
+
+ const handleSelectCase = (caseId: string) => {
+ setSelectedCaseId(caseId);
+ setCurrentView('workspace');
+ };
+
+ const handleLaunchAICase = (aiData: AIAnalysisResponse, rawText: string) => {
+ const newCaseId = `case-${Date.now()}`;
+ const formattedFacts: ExtractedFact[] = aiData.facts.map((f, idx) => ({
+ id: `fact-${Date.now()}-${idx}`,
+ label: f.label,
+ value: f.value,
+ }));
+
+ const formattedPii = (aiData.piiItems || []).map((p, idx) => ({
+ id: `pii-${Date.now()}-${idx}`,
+ original: p.original,
+ type: p.type,
+ masked: true,
+ }));
+
+ const todayStr = new Intl.DateTimeFormat('en-IN', {
+ month: 'long',
+ day: 'numeric',
+ year: 'numeric',
+ }).format(new Date());
+
+ const newCase: LegalCase = {
+ id: newCaseId,
+ caseNumber: `#CL-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`,
+ title: aiData.title,
+ category: aiData.category,
+ rulebookId: aiData.rulebookId,
+ status: 'drafting',
+ statusBadge: 'Assessed',
+ statusType: 'success',
+ deadlineText: `Reply required in ${aiData.daysRemaining} days`,
+ filedDate: todayStr,
+ description: aiData.formalLetter || aiData.legalDiagnosis,
+ formalLetter: aiData.formalLetter,
+ rawIntakeText: rawText,
+ redactedText: aiData.redactedText,
+ piiItems:
+ formattedPii.length > 0
+ ? formattedPii
+ : [{ id: `pii-${Date.now()}-1`, original: 'Citizen Requester', type: 'Identity', masked: true }],
+ facts:
+ formattedFacts.length > 0
+ ? formattedFacts
+ : [{ id: `fact-${Date.now()}-1`, label: 'Primary Grievance', value: rawText }],
+ officer: {
+ name: aiData.officer.name,
+ title: aiData.officer.title,
+ department: aiData.officer.department,
+ avatar: aiData.officer.avatar || 'PO',
+ jurisdiction: aiData.officer.jurisdiction || selectedCity,
+ email: aiData.officer.email,
+ address: aiData.officer.address,
+ portalUrl: aiData.officer.portalUrl,
+ designationNote: aiData.officer.designationNote,
+ },
+ requestScope: aiData.requestScope,
+ dateRange: aiData.dateRange || 'Past 90 Days',
+ statute: aiData.statute,
+ score: aiData.initialScore || 85,
+ hasHardshipClause: false,
+ autoRedactEnabled: true,
+ autoEscalateEnabled: true,
+ daysRemaining: aiData.daysRemaining || 18,
+ revisionCount: 1,
+ lastEdited: 'Just now',
+ vulnerabilities: aiData.vulnerabilities,
+ recommendedFixes: aiData.recommendedFixes,
+ appellateStrategy: aiData.appellateStrategy,
+ statutoryClauses: aiData.statutoryClauses,
+ recommendedExhibits: aiData.recommendedExhibits,
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+ setSelectedCaseId(newCaseId);
+
+ if (user) {
+ saveUserCaseToFirestore(user.uid, newCase)
+ .then(() => showToast('Case saved and synced to your Firebase Cloud'))
+ .catch((err) => console.error('Cloud save error:', err));
+ }
+
+ setCurrentView('workspace');
+ };
+
+ const handleLaunchCustomDraft = (draftText: string, caseTitle: string) => {
+ const newCaseId = `case-${Date.now()}`;
+ const todayStr = new Intl.DateTimeFormat('en-IN', {
+ month: 'long',
+ day: 'numeric',
+ year: 'numeric',
+ }).format(new Date());
+
+ const newCase: LegalCase = {
+ id: newCaseId,
+ caseNumber: `#CL-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`,
+ title: caseTitle || 'Right to Information Notice',
+ category: 'Public Records (RTI)',
+ rulebookId: 'rti',
+ status: 'drafting',
+ statusBadge: 'RTI Audited',
+ statusType: 'success',
+ deadlineText: 'Reply required in 30 days',
+ filedDate: todayStr,
+ description: draftText,
+ formalLetter: draftText,
+ rawIntakeText: draftText,
+ redactedText: draftText,
+ piiItems: [],
+ facts: [{ id: `fact-1`, label: 'RTI Scope', value: 'Statutory inspection & certified records request.' }],
+ officer: {
+ name: 'Public Information Officer (PIO)',
+ title: 'Designated CPIO / PIO',
+ department: 'Public Authority Division',
+ avatar: 'PIO',
+ jurisdiction: selectedCity,
+ email: 'rti.cell@nic.in',
+ address: `${selectedCity} Central Government / Civic Complex`,
+ portalUrl: 'https://rtionline.gov.in',
+ },
+ requestScope: 'Certified copies under Section 6(1) and Section 2(j) of RTI Act 2005.',
+ dateRange: 'Relevant Financial Year',
+ statute: 'Right to Information Act 2005 § 6(1)',
+ score: 95,
+ hasHardshipClause: false,
+ autoRedactEnabled: true,
+ autoEscalateEnabled: true,
+ daysRemaining: 30,
+ revisionCount: 1,
+ lastEdited: 'Just now',
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+ setSelectedCaseId(newCaseId);
+
+ if (user) {
+ saveUserCaseToFirestore(user.uid, newCase)
+ .then(() => showToast('RTI Filing saved to Firebase Firestore'))
+ .catch((err) => console.error('Cloud save error:', err));
+ }
+
+ setCurrentView('workspace');
+ };
+
+ const handleSaveAICaseToCloud = async (aiData: AIAnalysisResponse, rawText: string) => {
+ const newCaseId = `case-${Date.now()}`;
+ const formattedFacts: ExtractedFact[] = aiData.facts.map((f, idx) => ({
+ id: `fact-${Date.now()}-${idx}`,
+ label: f.label,
+ value: f.value,
+ }));
+
+ const formattedPii = (aiData.piiItems || []).map((p, idx) => ({
+ id: `pii-${Date.now()}-${idx}`,
+ original: p.original,
+ type: p.type,
+ masked: true,
+ }));
+
+ const todayStr = new Intl.DateTimeFormat('en-IN', {
+ month: 'long',
+ day: 'numeric',
+ year: 'numeric',
+ }).format(new Date());
+
+ const newCase: LegalCase = {
+ id: newCaseId,
+ caseNumber: `#CL-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`,
+ title: aiData.title,
+ category: aiData.category,
+ rulebookId: aiData.rulebookId,
+ status: 'drafting',
+ statusBadge: 'Assessed',
+ statusType: 'success',
+ deadlineText: `Reply required in ${aiData.daysRemaining} days`,
+ filedDate: todayStr,
+ description: aiData.formalLetter || aiData.legalDiagnosis,
+ formalLetter: aiData.formalLetter,
+ rawIntakeText: rawText,
+ redactedText: aiData.redactedText,
+ piiItems: formattedPii,
+ facts: formattedFacts,
+ officer: {
+ name: aiData.officer.name,
+ title: aiData.officer.title,
+ department: aiData.officer.department,
+ avatar: aiData.officer.avatar || 'PO',
+ jurisdiction: aiData.officer.jurisdiction || selectedCity,
+ email: aiData.officer.email,
+ address: aiData.officer.address,
+ portalUrl: aiData.officer.portalUrl,
+ designationNote: aiData.officer.designationNote,
+ },
+ requestScope: aiData.requestScope,
+ dateRange: aiData.dateRange || 'Past 90 Days',
+ statute: aiData.statute,
+ score: aiData.initialScore || 85,
+ hasHardshipClause: false,
+ autoRedactEnabled: true,
+ autoEscalateEnabled: true,
+ daysRemaining: aiData.daysRemaining || 18,
+ revisionCount: 1,
+ lastEdited: 'Just now',
+ vulnerabilities: aiData.vulnerabilities,
+ recommendedFixes: aiData.recommendedFixes,
+ appellateStrategy: aiData.appellateStrategy,
+ statutoryClauses: aiData.statutoryClauses,
+ recommendedExhibits: aiData.recommendedExhibits,
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+
+ if (user) {
+ await saveUserCaseToFirestore(user.uid, newCase);
+ showToast('Filing saved to Firebase Firestore Cloud');
+ } else {
+ showToast('Saved locally. Sign in to enable permanent cloud backup.');
+ }
+ };
+
+ const handleSelectRulebook = (rulebookId: string, initialPrompt?: string) => {
+ let title = 'New Filing Draft';
+ let category = 'Public Records (RTI)';
+ let scope = 'Certified copies of files, tender details, and official note sheets.';
+ let statute = 'Right to Information Act 2005 § 6(1)';
+
+ if (rulebookId === 'consumer') {
+ title = 'Consumer Dispute - Defective Product & Deficient Service';
+ category = 'Consumer Disputes';
+ scope = 'Formal 15-day statutory notice for full refund and compensation.';
+ statute = 'Consumer Protection Act 2019 § 35';
+ } else if (rulebookId === 'tenancy') {
+ title = 'Tenancy Dispute - Security Deposit Recovery';
+ category = 'Tenancy Protections';
+ scope = 'Statutory notice demanding immediate return of withheld security deposit with interest.';
+ statute = 'State Tenancy & Rent Control Act';
+ } else if (rulebookId === 'employment') {
+ title = 'Wage Recovery & Final Settlement Demand';
+ category = 'Labor & Employment Law';
+ scope = 'Demand for immediate release of unpaid salary, F&F settlement, and relieving certificate.';
+ statute = 'Payment of Wages Act 1936 § 15';
+ }
+
+ const newCaseId = `case-${Date.now()}`;
+ const newCase: LegalCase = {
+ ...currentCase,
+ id: newCaseId,
+ caseNumber: `#CL-2025-${Math.floor(1000 + Math.random() * 9000)}`,
+ title: initialPrompt ? `Case: ${initialPrompt.slice(0, 45)}...` : title,
+ category,
+ rulebookId,
+ requestScope: initialPrompt ? initialPrompt : scope,
+ statute,
+ filedDate: 'Current Filing',
+ status: 'drafting',
+ statusBadge: 'In Progress',
+ statusType: 'warning',
+ score: 85,
+ hasHardshipClause: false,
+ autoRedactEnabled: true,
+ autoEscalateEnabled: false,
+ daysRemaining: 30,
+ lastEdited: 'Just now',
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+ setSelectedCaseId(newCaseId);
+
+ if (user) {
+ saveUserCaseToFirestore(user.uid, newCase).catch((err) =>
+ console.error('Error saving template case to cloud:', err)
+ );
+ }
+
+ setCurrentView('workspace');
+ };
+
+ const handleDeleteCase = (caseId: string) => {
+ setCases((prev) => prev.filter((c) => c.id !== caseId));
+ if (user) {
+ deleteUserCaseFromFirestore(user.uid, caseId)
+ .then(() => showToast('Case filing deleted from cloud'))
+ .catch((err) => console.error('Error deleting from Firestore:', err));
+ } else {
+ showToast('Case removed');
+ }
+ };
+
+ const handleDuplicateCase = (legalCase: LegalCase) => {
+ const duplicatedId = `case-${Date.now()}`;
+ const duplicatedCase: LegalCase = {
+ ...legalCase,
+ id: duplicatedId,
+ caseNumber: `#CL-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`,
+ title: `${legalCase.title} (Copy)`,
+ lastEdited: 'Just now',
+ revisionCount: 1,
+ };
+
+ setCases((prev) => [duplicatedCase, ...prev]);
+ if (user) {
+ saveUserCaseToFirestore(user.uid, duplicatedCase)
+ .then(() => showToast('Case duplicated and saved in Cloud'))
+ .catch((err) => console.error('Error saving duplicate to Firestore:', err));
+ } else {
+ showToast('Case duplicated');
+ }
+ };
+
+ const handleNewFiling = () => {
+ setShowGuidedIntake(true);
+ };
+
+ const handleOpenGuidedIntakeWithCategory = (category?: string) => {
+ setGuidedIntakeCategory(category);
+ setShowGuidedIntake(true);
+ };
+
+ const handleDownloadPDF = (legalCase: LegalCase) => {
+ setCaseToPDF(legalCase);
+ };
+
+ const handleShareCase = (legalCase: LegalCase) => {
+ setCaseToShare(legalCase);
+ };
+
+ const handleOpenQuickAction = (action: 'appeal' | 'history' | 'rulebooks') => {
+ if (action === 'appeal') setShowAppealTracker(true);
+ if (action === 'history') setShowRevisionHistory(true);
+ if (action === 'rulebooks') setShowRulebooksModal(true);
+ };
+
+ return (
+
+ {/* Top Notification Toast */}
+ {toastMessage && (
+
+ check_circle
+ {toastMessage}
+
+ )}
+
+ {/* TopNavBar Header */}
+
+
+ {/* Main View Router */}
+
+ {currentView === 'home' && (
+ setShowRulebooksModal(true)}
+ onAnalyzePrompt={() => {}}
+ onOpenGuidedIntake={handleOpenGuidedIntakeWithCategory}
+ onOpenRtiAuditor={() => setShowRtiAuditor(true)}
+ />
+ )}
+
+ {currentView === 'workspace' && (
+
+ )}
+
+ {currentView === 'my-filings' && (
+
+ )}
+
+
+ {/* Footer */}
+
{
+ if (type === 'rulebooks') {
+ setShowRulebooksModal(true);
+ } else {
+ setLegalModalType(type);
+ }
+ }}
+ />
+
+ {/* Modals */}
+ {showGuidedIntake && (
+ {
+ setShowGuidedIntake(false);
+ setGuidedIntakeCategory(undefined);
+ }}
+ onLaunchCase={handleLaunchAICase}
+ />
+ )}
+
+ {showRtiAuditor && (
+ setShowRtiAuditor(false)}
+ onLaunchRtiCase={handleLaunchCustomDraft}
+ />
+ )}
+
+ {showRulebooksModal && (
+ setShowRulebooksModal(false)}
+ />
+ )}
+
+ {showAppealTracker && (
+ setShowAppealTracker(false)}
+ onOpenWorkspace={() => setCurrentView('workspace')}
+ />
+ )}
+
+ {showRevisionHistory && (
+ setShowRevisionHistory(false)} />
+ )}
+
+ {caseToShare && (
+ setCaseToShare(null)}
+ />
+ )}
+
+ {caseToPDF && (
+ setCaseToPDF(null)}
+ />
+ )}
+
+ {legalModalType && (
+ setLegalModalType(null)}
+ />
+ )}
+
+ );
+}
+
diff --git a/Frontend/src/README.md b/Frontend/src/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d3f5a12faa99758192ecc4ed3fc22c9249232e86
--- /dev/null
+++ b/Frontend/src/README.md
@@ -0,0 +1 @@
+
diff --git a/Frontend/src/components/AppealTrackerModal.tsx b/Frontend/src/components/AppealTrackerModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..993dbd8390ea230ae3b5c82293a7698086d60c5e
--- /dev/null
+++ b/Frontend/src/components/AppealTrackerModal.tsx
@@ -0,0 +1,151 @@
+import React from 'react';
+
+interface AppealTrackerModalProps {
+ onClose: () => void;
+ onOpenWorkspace: () => void;
+}
+
+export const AppealTrackerModal: React.FC = ({
+ onClose,
+ onOpenWorkspace,
+}) => {
+ const milestones = [
+ {
+ title: 'Formal Filing Submitted',
+ date: 'Oct 24, 2024 • 10:14 AM',
+ description: 'Request dispatched to Sgt. Amanda Reyes via electronic statutory gateway.',
+ status: 'completed',
+ },
+ {
+ title: 'Agency Acknowledgement & Tracking ID',
+ date: 'Oct 24, 2024 • 02:40 PM',
+ description: 'Formal confirmation received. Reference ID: #SPD-REC-8932.',
+ status: 'completed',
+ },
+ {
+ title: 'PIO Records Custodian Review',
+ date: 'In Progress • 18 Days Remaining',
+ description: 'Records Division reviewing bodycam video timestamps for redactions.',
+ status: 'current',
+ },
+ {
+ title: 'Release of Non-Exempt Records',
+ date: 'Est. Nov 11, 2024',
+ description: 'Electronic PDF/MP4 link generation and delivery to requester vault.',
+ status: 'upcoming',
+ },
+ {
+ title: 'First Appellate Authority Escalation (If Delayed)',
+ date: 'Automated if no response by Day 20',
+ description: 'Automatic generation of Section 19 First Appeal petition.',
+ status: 'upcoming',
+ },
+ ];
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ track_changes
+
+ Appeal & Filing Tracker
+
+
+
+ Case #CL-2024-8932 • Public Records Request
+
+
+
+ close
+
+
+
+ {/* Timeline body */}
+
+
+
+
+ verified
+
+
+
+ Statutory Clock: On Schedule
+
+
+ Agency response window expires in 18 calendar days.
+
+
+
+
+ Healthy
+
+
+
+
+ {milestones.map((m, idx) => {
+ const isCompleted = m.status === 'completed';
+ const isCurrent = m.status === 'current';
+
+ return (
+
+ {/* Step icon dot */}
+
+ {isCompleted ? (
+ check
+ ) : (
+ idx + 1
+ )}
+
+
+
+
+
+ {m.title}
+
+ {m.date}
+
+
{m.description}
+
+
+ );
+ })}
+
+
+
+ {/* Footer */}
+
+
+ Close
+
+ {
+ onClose();
+ onOpenWorkspace();
+ }}
+ className="px-4 py-2 text-[13px] font-medium bg-[#0F172A] text-white hover:bg-[#1E293B] rounded-lg cursor-pointer flex items-center gap-1"
+ >
+ Open Case in Workspace
+ arrow_forward
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/AuthModal.tsx b/Frontend/src/components/AuthModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..70dc9557b3d9ae1681f500730b6194e1325885f3
--- /dev/null
+++ b/Frontend/src/components/AuthModal.tsx
@@ -0,0 +1,357 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useAuth } from '../context/AuthContext';
+
+interface AuthModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export const AuthModal: React.FC = ({ isOpen, onClose }) => {
+ const { signIn, signInEmail, signUpEmail, authError, clearAuthError } = useAuth();
+ const [authMode, setAuthMode] = useState<'signin' | 'signup' | 'verification_sent'>('signin');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [displayName, setDisplayName] = useState('');
+ const [loadingGoogle, setLoadingGoogle] = useState(false);
+ const [loadingEmail, setLoadingEmail] = useState(false);
+ const [localError, setLocalError] = useState(null);
+ const [successInfo, setSuccessInfo] = useState(null);
+ const modalContentRef = useRef(null);
+
+ // Close on Escape key press
+ useEffect(() => {
+ if (!isOpen) return;
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ onClose();
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [isOpen, onClose]);
+
+ if (!isOpen) return null;
+
+ const handleGoogleAuth = async () => {
+ setLoadingGoogle(true);
+ setLocalError(null);
+ setSuccessInfo(null);
+ clearAuthError();
+ try {
+ await signIn();
+ onClose();
+ } catch (err: any) {
+ if (err?.code === 'auth/unauthorized-domain' || err?.message?.includes('authorized')) {
+ setLocalError(
+ 'Google Sign-in: This domain is not listed in your Firebase Authorized Domains. You can use Email Sign-in below or add your domain in Firebase Console.'
+ );
+ } else {
+ setLocalError(err?.message || 'Google sign-in popup was closed.');
+ }
+ } finally {
+ setLoadingGoogle(false);
+ }
+ };
+
+ const handleEmailAuth = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!email.trim() || !password.trim()) {
+ setLocalError('Please enter both email and password.');
+ return;
+ }
+
+ setLoadingEmail(true);
+ setLocalError(null);
+ setSuccessInfo(null);
+ clearAuthError();
+
+ try {
+ if (authMode === 'signup') {
+ await signUpEmail(email.trim(), password, displayName.trim());
+ setAuthMode('verification_sent');
+ setSuccessInfo(
+ `Account created for ${email.trim()}! Please open your email inbox, click the verification link, and then sign in.`
+ );
+ } else {
+ await signInEmail(email.trim(), password);
+ onClose();
+ }
+ } catch (err: any) {
+ if (err?.code === 'auth/verification-sent') {
+ setAuthMode('verification_sent');
+ setSuccessInfo(err.message);
+ } else if (err?.code === 'auth/unverified-email') {
+ setAuthMode('verification_sent');
+ setSuccessInfo(err.message);
+ } else {
+ setLocalError(err?.message || 'Authentication failed. Please check your details.');
+ }
+ } finally {
+ setLoadingEmail(false);
+ }
+ };
+
+ return (
+
+
e.stopPropagation()}
+ className="bg-white rounded-2xl max-w-md w-full shadow-2xl border border-[#CBD5E1] overflow-hidden flex flex-col cursor-default"
+ >
+ {/* Header */}
+
+
+
gavel
+
+
+ {authMode === 'signin' ? 'Sign in to CaseLoop' : 'Create Free Account'}
+
+
Sync your cases & legal notice drafts
+
+
+
+ close
+
+
+
+ {/* Body */}
+
+ {(localError || authError) && (
+
+
+ info
+
+
+
Notice
+
{localError || authError}
+
+
+ )}
+
+ {authMode === 'verification_sent' ? (
+
+
+ mark_email_unread
+
+
+ Check Your Inbox to Verify
+
+
+ {successInfo ||
+ `We've sent a verification link to ${email || 'your email'}. Please click the link to activate your account before logging in.`}
+
+
+
+
+ check_circle
+ 1. Open the verification email
+
+
+ check_circle
+ 2. Click the confirmation link
+
+
+ check_circle
+ 3. Return here and Sign In
+
+
+
+
+ {
+ setAuthMode('signin');
+ setLocalError(null);
+ setSuccessInfo(null);
+ }}
+ className="w-full py-3 bg-[#006c4a] hover:bg-[#005137] text-white font-bold rounded-xl text-[14px] transition-all cursor-pointer shadow-md"
+ >
+ I have verified my email — Sign In
+
+ {
+ if (email.trim() && password.trim()) {
+ try {
+ setLoadingEmail(true);
+ await signInEmail(email.trim(), password);
+ } catch (err: any) {
+ setLocalError('Verification email resent to ' + email.trim());
+ } finally {
+ setLoadingEmail(false);
+ }
+ } else {
+ setAuthMode('signin');
+ }
+ }}
+ className="w-full py-2.5 bg-[#F1F5F9] hover:bg-[#E2E8F0] text-[#0F172A] font-semibold rounded-xl text-[13px] transition-all cursor-pointer"
+ >
+ Resend Verification Email
+
+
+
+ ) : (
+ <>
+ {/* Option 1: Continue with Google */}
+
+ {loadingGoogle ? (
+ <>
+ sync
+ Connecting to Google...
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+ Continue with Google
+ >
+ )}
+
+
+
+
+
+ OR EMAIL LOGIN
+
+
+
+
+ {/* Toggle between Sign in and Sign up */}
+
+ {
+ setAuthMode('signin');
+ setLocalError(null);
+ clearAuthError();
+ }}
+ className={`flex-1 py-1.5 text-[13px] font-semibold rounded-lg transition-all cursor-pointer ${
+ authMode === 'signin'
+ ? 'bg-white text-[#0F172A] shadow-xs'
+ : 'text-[#64748B] hover:text-[#0F172A]'
+ }`}
+ >
+ Sign In
+
+ {
+ setAuthMode('signup');
+ setLocalError(null);
+ clearAuthError();
+ }}
+ className={`flex-1 py-1.5 text-[13px] font-semibold rounded-lg transition-all cursor-pointer ${
+ authMode === 'signup'
+ ? 'bg-white text-[#0F172A] shadow-xs'
+ : 'text-[#64748B] hover:text-[#0F172A]'
+ }`}
+ >
+ Create Account
+
+
+
+ {/* Option 2: Email Form */}
+
+ >
+ )}
+
+
+ {/* Footer */}
+
+ Protected with Firebase Auth
+
+ Cancel
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/FilingWorkspace.tsx b/Frontend/src/components/FilingWorkspace.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eabccc45c069a2ef822e37ba5f411ed6e016cf88
--- /dev/null
+++ b/Frontend/src/components/FilingWorkspace.tsx
@@ -0,0 +1,992 @@
+import React, { useState } from 'react';
+import { LegalCase, ExtractedFact } from '../types';
+import { DECODED_RESPONSES } from '../data/mockData';
+import {
+ generateFilingDraftApi,
+ stressTestApi,
+ interpretResponseApi,
+ searchOfficerApi,
+ AIStressTestResponse,
+ AIDecodedResponse,
+ AuthoritySearchResult,
+} from '../services/api';
+
+interface FilingWorkspaceProps {
+ currentCase: LegalCase;
+ onUpdateCase: (updatedCase: LegalCase) => void;
+ onNavigate: (view: 'home' | 'workspace' | 'my-filings') => void;
+ onDownloadPDF: (legalCase: LegalCase) => void;
+}
+
+export const FilingWorkspace: React.FC = ({
+ currentCase,
+ onUpdateCase,
+ onNavigate,
+ onDownloadPDF,
+}) => {
+ // Step Tabs: 1 to 6
+ const [activeStep, setActiveStep] = useState(4);
+ const [copiedDraft, setCopiedDraft] = useState(false);
+
+ // AI draft state
+ const [generatingDraft, setGeneratingDraft] = useState(false);
+ const [draftNoticeText, setDraftNoticeText] = useState(
+ currentCase.formalLetter ||
+ (currentCase.description?.includes('To:') || currentCase.description?.includes('NOTICE')
+ ? currentCase.description
+ : `LEGAL NOTICE & STATUTORY DEMAND
+Pursuant to ${currentCase.statute || 'Statutory Code of Procedure'}
+
+To:
+${currentCase.officer?.name || 'Public Information Officer / Authorized Officer'}
+${currentCase.officer?.title || 'Designated Public Officer'}
+${currentCase.officer?.department || 'Grievance Redressal Division'}
+${currentCase.officer?.address || `${currentCase.officer?.jurisdiction || 'Local'} Administrative Complex`}
+Email: ${currentCase.officer?.email || 'records.intake@civic-gateway.gov'}
+
+Date: ${currentCase.filedDate || new Intl.DateTimeFormat('en-IN', { month: 'long', day: 'numeric', year: 'numeric' }).format(new Date())}
+Case Reference: ${currentCase.caseNumber}
+
+SUBJECT: STATUTORY DEMAND NOTICE IN THE MATTER OF: ${currentCase.title.toUpperCase()}
+
+Sir / Madam,
+
+Under instructions from and on behalf of the Requester, this formal statutory notice is served upon you regarding the following matter:
+
+1. STATEMENT OF MATERIAL FACTS:
+${(currentCase.facts || []).map((f, i) => `(${String.fromCharCode(97 + i)}) ${f.label}: ${f.value}`).join('\n') || `(a) Core Matter: ${currentCase.rawIntakeText || currentCase.description}`}
+
+2. STATUTORY GROUNDS & APPLICABLE LAWS:
+(a) The failure to address this matter violates the mandatory provisions of ${currentCase.statute}.
+(b) You are statutorily required to resolve the grievance, furnish certified records, and ensure complete compliance without arbitrary delay.
+
+3. SPECIFIC DEMANDS & REMEDIES:
+(i) ${currentCase.requestScope || 'Take immediate remedial action and resolve the grievance in full.'}
+(ii) Provide complete written reasons and certified copies of file notesheets concerning the action taken.
+(iii) Respond within the mandatory statutory period of ${currentCase.daysRemaining || 30} days from receipt.
+
+4. NOTICE OF LEGAL ACTION & COST LIABILITY:
+Take notice that in default of compliance within the stipulated period, the Requester shall initiate appropriate appellate and judicial proceedings before the competent Forum / Commission / Court, holding you liable for all costs and statutory interest.
+
+Yours faithfully,
+${currentCase.autoRedactEnabled ? '[REDACTED_REQUESTER]' : 'Authorized Requester'}`)
+ );
+
+ // Custom Prompt for re-drafting
+ const [customDraftPrompt, setCustomDraftPrompt] = useState('');
+
+ // Stress test state
+ const [stressTesting, setStressTesting] = useState(false);
+ const [stressResult, setStressResult] = useState(
+ currentCase.vulnerabilities
+ ? {
+ score: currentCase.score || 88,
+ grade: (currentCase.score || 88) >= 90 ? 'A+' : 'A',
+ vulnerabilities: (currentCase.vulnerabilities || []).map((v) => ({
+ title: 'Potential Opposition Maneuver',
+ description: v,
+ impact: 'medium',
+ fixAvailable: true,
+ fixActionText: 'Apply Statutory Counter-Clause',
+ })),
+ strengths: [
+ `Clear statutory basis established under ${currentCase.statute}`,
+ 'Structured chronological statement of facts',
+ 'Specific demand and compliance timeline served',
+ ],
+ recommendedFixes: currentCase.recommendedFixes || [],
+ appellateStrategy: currentCase.appellateStrategy || 'First Appeal before First Appellate Authority / Consumer Commission',
+ }
+ : null
+ );
+
+ // Agency Response Decoder state
+ const [customAgencyText, setCustomAgencyText] = useState('');
+ const [decodingResponse, setDecodingResponse] = useState(false);
+ const [decodedResult, setDecodedResult] = useState(null);
+ const [selectedPresetIndex] = useState(0);
+
+ // Facts state
+ const [showAddFact, setShowAddFact] = useState(false);
+ const [newFactLabel, setNewFactLabel] = useState('');
+ const [newFactValue, setNewFactValue] = useState('');
+
+ // Officer Directory Search state
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchingOfficer, setSearchingOfficer] = useState(false);
+ const [searchResult, setSearchResult] = useState(null);
+ const [searchError, setSearchError] = useState(null);
+
+ // Generate / Re-draft AI Notice
+ const handleGenerateAIDraft = async (specialInstruction?: string) => {
+ setGeneratingDraft(true);
+ try {
+ const instructions = specialInstruction || customDraftPrompt || undefined;
+ const res = await generateFilingDraftApi(currentCase, instructions);
+ if (res.formalLetter) {
+ setDraftNoticeText(res.formalLetter);
+ onUpdateCase({
+ ...currentCase,
+ formalLetter: res.formalLetter,
+ description: res.formalLetter,
+ statutoryClauses: res.statutoryClauses,
+ recommendedExhibits: res.recommendedExhibits,
+ });
+ }
+ } catch (err) {
+ console.error('Failed to generate AI draft:', err);
+ } finally {
+ setGeneratingDraft(false);
+ }
+ };
+
+ // Run AI Stress Test
+ const handleRunStressTest = async () => {
+ setStressTesting(true);
+ try {
+ const res = await stressTestApi(currentCase);
+ setStressResult(res);
+ if (res.score) {
+ onUpdateCase({
+ ...currentCase,
+ score: res.score,
+ vulnerabilities: res.vulnerabilities?.map((v) => v.description),
+ recommendedFixes: res.recommendedFixes,
+ appellateStrategy: res.appellateStrategy,
+ });
+ }
+ } catch (err) {
+ console.error('Failed to run AI stress test:', err);
+ } finally {
+ setStressTesting(false);
+ }
+ };
+
+ // Search Live Authority Directory
+ const handleSearchDirectory = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!searchQuery.trim()) return;
+
+ setSearchingOfficer(true);
+ setSearchError(null);
+ try {
+ const res = await searchOfficerApi(
+ searchQuery.trim(),
+ currentCase.officer?.jurisdiction || 'Bengaluru'
+ );
+ setSearchResult(res);
+ } catch (err: any) {
+ console.error('Directory search error:', err);
+ setSearchError('Could not find contacts for this department. Please refine your search.');
+ } finally {
+ setSearchingOfficer(false);
+ }
+ };
+
+ // Apply Discovered Authority Contacts to Case
+ const handleApplyDiscoveredOfficer = () => {
+ if (!searchResult) return;
+
+ const updatedOfficer = {
+ ...currentCase.officer,
+ name: searchResult.officerTitle.split('&')[0]?.trim() || currentCase.officer?.name || 'Designated Public Officer',
+ title: searchResult.officerTitle,
+ department: searchResult.department,
+ email: searchResult.nodalEmail,
+ address: searchResult.officeAddress,
+ portalUrl: searchResult.grievancePortal,
+ };
+
+ const updatedCase = {
+ ...currentCase,
+ officer: updatedOfficer,
+ statute: searchResult.statute || currentCase.statute,
+ };
+
+ onUpdateCase(updatedCase);
+
+ // Also update draft notice recipient header if possible
+ const updatedDraft = draftNoticeText.replace(
+ /To:[\s\S]*?Date:/i,
+ `To:\n${updatedOfficer.name}\n${updatedOfficer.title}, ${updatedOfficer.department}\n${updatedOfficer.address}\nEmail: ${updatedOfficer.email}\n\nDate:`
+ );
+ setDraftNoticeText(updatedDraft);
+ };
+
+ // Decode Agency Response
+ const handleDecodeResponse = async () => {
+ const textToDecode = customAgencyText.trim() || DECODED_RESPONSES[selectedPresetIndex]?.departmentStatement || '';
+ if (!textToDecode) return;
+
+ setDecodingResponse(true);
+ try {
+ const res = await interpretResponseApi(textToDecode, currentCase.category);
+ setDecodedResult(res);
+ } catch (err) {
+ console.error('Failed to decode response:', err);
+ } finally {
+ setDecodingResponse(false);
+ }
+ };
+
+ // Copy Draft Notice
+ const handleCopyDraft = () => {
+ navigator.clipboard.writeText(draftNoticeText);
+ setCopiedDraft(true);
+ setTimeout(() => setCopiedDraft(false), 2500);
+ };
+
+ // Toggle PII mask
+ const togglePiiMask = (id: string) => {
+ const updated = (currentCase.piiItems || []).map((item) =>
+ item.id === id ? { ...item, masked: !item.masked } : item
+ );
+ onUpdateCase({ ...currentCase, piiItems: updated });
+ };
+
+ // Toggle Hardship clause
+ const toggleHardshipClause = () => {
+ const nextVal = !currentCase.hasHardshipClause;
+ const newScore = nextVal ? Math.min(98, (currentCase.score || 84) + 12) : Math.max(70, (currentCase.score || 84) - 12);
+ onUpdateCase({
+ ...currentCase,
+ hasHardshipClause: nextVal,
+ score: newScore,
+ });
+ };
+
+ // Add Fact
+ const handleAddFact = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!newFactLabel.trim() || !newFactValue.trim()) return;
+
+ const newFact: ExtractedFact = {
+ id: `fact-${Date.now()}`,
+ label: newFactLabel.trim(),
+ value: newFactValue.trim(),
+ };
+
+ onUpdateCase({
+ ...currentCase,
+ facts: [...(currentCase.facts || []), newFact],
+ });
+
+ setNewFactLabel('');
+ setNewFactValue('');
+ setShowAddFact(false);
+ };
+
+ // Remove Fact
+ const handleRemoveFact = (id: string) => {
+ onUpdateCase({
+ ...currentCase,
+ facts: (currentCase.facts || []).filter((f) => f.id !== id),
+ });
+ };
+
+ // Step definition
+ const STEPS = [
+ { num: 1, label: 'Hide Private Info', icon: 'shield' },
+ { num: 2, label: 'Who to Send It To', icon: 'account_box' },
+ { num: 3, label: 'What Happened', icon: 'fact_check' },
+ { num: 4, label: 'Official Letter', icon: 'edit_document' },
+ { num: 5, label: 'Check Weak Points', icon: 'health_and_safety' },
+ { num: 6, label: 'Translate Official Reply', icon: 'translate' },
+ ];
+
+ return (
+
+ {/* Top Breadcrumb & Status Header */}
+
+
+
+ onNavigate('home')}
+ className="hover:text-[#006c4a] flex items-center gap-1 cursor-pointer font-medium"
+ >
+ arrow_back
+ Home
+
+ /
+ onNavigate('my-filings')}
+ className="hover:text-[#006c4a] cursor-pointer font-medium"
+ >
+ My Filings
+
+ /
+ {currentCase.caseNumber}
+
+
+
+
+ {currentCase.title}
+
+
+ {currentCase.category}
+
+
+
+
+
+ account_balance
+ Law: {currentCase.statute}
+
+ •
+
+ location_on
+ {currentCase.officer?.jurisdiction || 'Local Jurisdiction'}
+
+ •
+
+ timer
+ {currentCase.daysRemaining || 30} Days to Reply
+
+
+
+
+ {/* Action Controls */}
+
+
+ health_and_safety
+ Strength: {currentCase.score || 88}%
+
+
+
onDownloadPDF({ ...currentCase, formalLetter: draftNoticeText, description: draftNoticeText })}
+ className="bg-[#0F172A] hover:bg-[#1E293B] text-white px-4 py-2 rounded-xl text-[13px] font-semibold flex items-center gap-1.5 cursor-pointer shadow-xs transition-colors"
+ >
+ picture_as_pdf
+ Download PDF
+
+
+
+
+ {/* Step Tabs Navigation Bar */}
+
+ {STEPS.map((step) => {
+ const isActive = activeStep === step.num;
+ return (
+ setActiveStep(step.num)}
+ className={`flex-1 min-w-[130px] py-3 px-3 rounded-xl flex items-center justify-center gap-2 text-[13px] font-semibold transition-all cursor-pointer ${
+ isActive
+ ? 'bg-[#006c4a] text-white shadow-sm'
+ : 'bg-transparent text-[#475569] hover:bg-[#F1F5F9] hover:text-[#0F172A]'
+ }`}
+ >
+ {step.icon}
+ {step.label}
+
+ );
+ })}
+
+
+ {/* STEP 1: HIDE PRIVATE INFO */}
+ {activeStep === 1 && (
+
+
+
+
+ shield
+ Hide Private Information
+
+
+ Automatically masks your phone number, bank details, and personal address to keep your filing secure.
+
+
+
+
+ {(currentCase.piiItems || []).filter((p) => p.masked).length} of {(currentCase.piiItems || []).length} Hidden
+
+
+
+
+
+
+
+ Private details detected:
+
+
+ {(currentCase.piiItems || []).length === 0 ? (
+
+ No sensitive financial or personal identity details detected in this filing.
+
+ ) : (
+ (currentCase.piiItems || []).map((item) => (
+
+
+
+ {item.type}
+
+
{item.original}
+
+
togglePiiMask(item.id)}
+ className={`text-[12px] font-semibold px-3 py-1.5 rounded-lg cursor-pointer transition-colors ${
+ item.masked
+ ? 'bg-[#DCFCE7] text-[#166534] border border-[#BBF7D0]'
+ : 'bg-[#FEE2E2] text-[#991B1B] border border-[#FECACA]'
+ }`}
+ >
+ {item.masked ? '🔒 Hidden' : '👁️ Visible'}
+
+
+ ))
+ )}
+
+
+
+
+
+ Protected Text Preview:
+
+
+ {currentCase.redactedText || currentCase.rawIntakeText || currentCase.description}
+
+
+ info
+ Protected tokens (e.g. [REDACTED_REQUESTER]) ensure your private data is shielded when notices are served.
+
+
+
+
+ )}
+
+ {/* STEP 2: WHO TO SEND IT TO */}
+ {activeStep === 2 && (
+
+
+
+
+ account_box
+ Who to Send It To
+
+
+ Real, designated public authority, grievance nodal officer, or corporate legal cell in {currentCase.officer?.jurisdiction || 'your city'}.
+
+
+
+
+
+ {/* Primary Officer Card */}
+
+
+
+ {currentCase.officer?.avatar || 'PO'}
+
+
+
+ {currentCase.officer?.name || 'Public Information Officer'}
+
+
{currentCase.officer?.title || 'Designated Nodal Officer'}
+
+
+
+
+
+ Department:
+ {currentCase.officer?.department || 'Civic Grievance Redressal Division'}
+
+
+ Jurisdiction / Area:
+ {currentCase.officer?.jurisdiction || 'Local Office'}
+
+
+ Official Email:
+ {currentCase.officer?.email || 'records.intake@civic-gateway.gov'}
+
+ {currentCase.officer?.address && (
+
+ Office Address:
+ {currentCase.officer.address}
+
+ )}
+ {currentCase.officer?.portalUrl && (
+
+ )}
+
+
+
+ verified
+ Designated officer & legal timeline ({currentCase.daysRemaining || 30} days) active for this statutory filing.
+
+
+
+ {/* Live Indian Authority Directory Search */}
+
+
+
+ search
+ Search Real Department Directory
+
+
+ Look up real public authorities, consumer forums, police nodal cells, or municipal offices in {currentCase.officer?.jurisdiction || 'your city'}.
+
+
+
+
+
+ {searchError && (
+
+ {searchError}
+
+ )}
+
+ {searchResult && (
+
+
+ {searchResult.department}
+
+ Verified
+
+
+
{searchResult.officerTitle}
+
+ Email: {searchResult.nodalEmail}
+
+
+ Address: {searchResult.officeAddress}
+
+ {searchResult.grievancePortal && (
+
+ Portal: {searchResult.grievancePortal}
+
+ )}
+
+ check_circle
+ Use This Authority for My Notice
+
+
+ )}
+
+
+
+ )}
+
+ {/* STEP 3: WHAT HAPPENED */}
+ {activeStep === 3 && (
+
+
+
+
+ fact_check
+ What Happened (Chronological Facts)
+
+
+ Gemini synthesized your narrative into legally structured, chronological facts. You can edit or add points.
+
+
+
setShowAddFact(true)}
+ className="bg-[#006c4a] hover:bg-[#005137] text-white text-[12.5px] font-semibold px-3.5 py-2 rounded-xl flex items-center gap-1.5 cursor-pointer shadow-xs transition-colors"
+ >
+ add
+ Add Detail
+
+
+
+ {/* Add Fact Form */}
+ {showAddFact && (
+
+ )}
+
+
+ {(currentCase.facts || []).map((fact) => (
+
+
+
+ {fact.label}
+
+
{fact.value}
+
+
handleRemoveFact(fact.id)}
+ className="text-[#94A3B8] hover:text-[#DC2626] cursor-pointer p-1"
+ title="Remove detail"
+ >
+ delete
+
+
+ ))}
+
+
+
+
+ Specific Relief / Remedy Demanded:
+
+
+
+ )}
+
+ {/* STEP 4: OFFICIAL LETTER (READY TO SEND) */}
+ {activeStep === 4 && (
+
+
+
+
+ edit_document
+ Official Statutory Notice Letter
+
+
+ AI-analyzed, formally drafted legal notice with statutory citations, factual chronology, and penalty clauses.
+
+
+
+
+ handleGenerateAIDraft()}
+ disabled={generatingDraft}
+ className="bg-[#006c4a] hover:bg-[#005137] text-white px-3.5 py-2 rounded-xl text-[13px] font-semibold flex items-center gap-1.5 cursor-pointer shadow-xs transition-all disabled:opacity-50"
+ >
+
+ {generatingDraft ? 'sync' : 'auto_fix_high'}
+
+ {generatingDraft ? 'Drafting with Gemini...' : 'Rewrite with AI'}
+
+
+
+
+ {copiedDraft ? 'check' : 'content_copy'}
+
+ {copiedDraft ? 'Copied!' : 'Copy Letter'}
+
+
+
+
+ {/* Quick Legal Tuning & Informal-to-Formal AI Refinement */}
+
+
+ Quick Legal Presets:
+ handleGenerateAIDraft('Make notice more assertive, citing specific statutory penalties and personal cost liability.')}
+ disabled={generatingDraft}
+ className="bg-white hover:bg-[#E2E8F0] border border-[#CBD5E1] text-[#0F172A] px-2.5 py-1 rounded-lg text-[12px] font-medium cursor-pointer transition-colors"
+ >
+ ⚖️ High Assertiveness & Costs
+
+ handleGenerateAIDraft('Add explicit Section 10 Severability clause and certified inspection demand.')}
+ disabled={generatingDraft}
+ className="bg-white hover:bg-[#E2E8F0] border border-[#CBD5E1] text-[#0F172A] px-2.5 py-1 rounded-lg text-[12px] font-medium cursor-pointer transition-colors"
+ >
+ 📑 Section 10 Severability
+
+ handleGenerateAIDraft('Include 48-Hour Urgent Life & Liberty notice invocation.')}
+ disabled={generatingDraft}
+ className="bg-white hover:bg-[#E2E8F0] border border-[#CBD5E1] text-[#0F172A] px-2.5 py-1 rounded-lg text-[12px] font-medium cursor-pointer transition-colors"
+ >
+ ⚡ 48-Hour Urgency Clause
+
+
+
+ {/* Custom Informal-to-Formal Instruction Box */}
+
+ setCustomDraftPrompt(e.target.value)}
+ placeholder="Tell AI in plain words what to add or change (e.g. 'tell them I am a student, demand ₹25k mental agony compensation and 18% interest')..."
+ className="flex-1 bg-white border border-[#CBD5E1] rounded-xl px-3.5 py-2 text-[13px] text-[#0F172A] outline-none focus:border-[#006c4a]"
+ />
+ handleGenerateAIDraft()}
+ disabled={generatingDraft || !customDraftPrompt.trim()}
+ className="bg-[#006c4a] hover:bg-[#005137] text-white px-4 py-2 rounded-xl text-[12.5px] font-semibold flex items-center justify-center gap-1.5 cursor-pointer disabled:opacity-50 transition-colors shrink-0"
+ >
+
+ {generatingDraft ? 'sync' : 'auto_fix_high'}
+
+ {generatingDraft ? 'Drafting...' : 'Translate to Legal Letter'}
+
+
+
+
+ {/* Large, Clean Full-Text Notice Area */}
+
+
+ )}
+
+ {/* STEP 5: CHECK WEAK POINTS */}
+ {activeStep === 5 && (
+
+
+
+
+ health_and_safety
+ Adversarial Stress Test & Weak Points Scrutiny
+
+
+ Gemini rigorously tests your filing for opposing party defenses, delay tactics, and exemption loopholes.
+
+
+
+
+
+ {stressTesting ? 'sync' : 'security'}
+
+ {stressTesting ? 'Analyzing weak points...' : 'Re-Run Stress Test'}
+
+
+
+
+ {/* Vulnerabilities Detected */}
+
+
+ warning
+ Opposition Defenses & Bureaucratic Excuses:
+
+
+ {stressResult?.vulnerabilities?.map((vuln, idx) => (
+
+ {vuln.title ? `${vuln.title}: ` : ''}
+ {vuln.description || (vuln as any)}
+
+ )) || (
+ <>
+ Authority may claim that records involve third-party commercial confidence under Section 8(1)(d).
+ Department may claim search & duplication fees to delay disclosure.
+ >
+ )}
+
+
+
+ {/* Recommended Safeguards */}
+
+
+ verified_user
+ Statutory Safeguards & Loophole Fixes:
+
+
+ {stressResult?.recommendedFixes?.map((fix, idx) => (
+ {fix}
+ )) || (
+ <>
+ Demand certified segregation of non-exempt portions under Section 10(1).
+ Cite mandatory 5-day Section 6(3) departmental transfer duty.
+ >
+ )}
+
+
+
+ Public Interest Fee Waiver Clause
+
+ {currentCase.hasHardshipClause ? '✓ Applied (+12 Score)' : '+ Apply Fix'}
+
+
+
+
+
+ {/* Appellate Strategy */}
+ {stressResult?.appellateStrategy && (
+
+
+ alt_route
+ Statutory Appellate Strategy
+
+
+ {stressResult.appellateStrategy}
+
+
+ )}
+
+ )}
+
+ {/* STEP 6: TRANSLATE OFFICIAL REPLY */}
+ {activeStep === 6 && (
+
+
+
+
+ translate
+ Translate Official Reply
+
+
+ Received a vague, rejecting, or bureaucratic letter? Paste it here to uncover what it actually means and get a ready rebuttal.
+
+
+
+
+
+ {decodingResponse ? 'sync' : 'bolt'}
+
+ {decodingResponse ? 'Translating...' : 'Translate Reply'}
+
+
+
+
+
+ Paste the letter or message you received:
+
+
+
+ {/* Decoded Output */}
+
+
+ Plain English Legal Analysis:
+
+
+
+
What They Actually Mean
+
+ {decodedResult?.plainEnglish || decodedResult?.actualMeaning || 'They are using standard bureaucratic delay excuses hoping you miss the 30-day appeal deadline.'}
+
+
+
+
+
What You Should Do Next
+
+ {decodedResult?.recommendedNextStep || decodedResult?.recommendedAction || 'Serve a 48-hour Demand for Segregation or immediately file a First Appeal before the First Appellate Authority.'}
+
+
+
+
+ {decodedResult?.suggestedRebuttal && (
+
+
Ready-to-Send Statutory Rebuttal:
+
+ {decodedResult.suggestedRebuttal}
+
+
+ )}
+
+
+ )}
+
+ );
+};
diff --git a/Frontend/src/components/Footer.tsx b/Frontend/src/components/Footer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b4de1f5bea2d51ba0b5215e059348958c34dfdaa
--- /dev/null
+++ b/Frontend/src/components/Footer.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+
+interface FooterProps {
+ onOpenLegal: (type: 'disclaimer' | 'accessibility' | 'rulebooks' | 'privacy') => void;
+}
+
+export const Footer: React.FC = ({ onOpenLegal }) => {
+ return (
+
+
+
+
+ gavel
+ CaseLoop
+
+
+ © 2024 CaseLoop Civic-Tech. All rights reserved. Built for legal empowerment.
+
+
+
+
+ onOpenLegal('disclaimer')}
+ className="hover:text-[#0F172A] underline underline-offset-4 transition-colors cursor-pointer"
+ >
+ Legal Disclaimers
+
+ onOpenLegal('accessibility')}
+ className="hover:text-[#0F172A] underline underline-offset-4 transition-colors cursor-pointer"
+ >
+ Accessibility Statement
+
+ onOpenLegal('rulebooks')}
+ className="hover:text-[#0F172A] underline underline-offset-4 transition-colors cursor-pointer"
+ >
+ Rulebooks
+
+ onOpenLegal('privacy')}
+ className="hover:text-[#0F172A] underline underline-offset-4 transition-colors cursor-pointer"
+ >
+ Privacy Policy
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/GuidedIntakeModal.tsx b/Frontend/src/components/GuidedIntakeModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..cbad4f3bf93cca8a36b3f7f5fda4d7e67a38ab61
--- /dev/null
+++ b/Frontend/src/components/GuidedIntakeModal.tsx
@@ -0,0 +1,173 @@
+import React, { useState } from 'react';
+import { analyzeIssueApi, AIAnalysisResponse } from '../services/api';
+
+interface GuidedIntakeModalProps {
+ initialCategory?: string;
+ selectedCity: string;
+ onClose: () => void;
+ onLaunchCase: (aiData: AIAnalysisResponse, rawText: string) => void;
+}
+
+export const GuidedIntakeModal: React.FC = ({
+ initialCategory = 'rti',
+ selectedCity,
+ onClose,
+ onLaunchCase,
+}) => {
+ const [selectedModel, setSelectedModel] = useState(initialCategory);
+ const [grievanceText, setGrievanceText] = useState('');
+ const [attachedFile, setAttachedFile] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleProcess = async () => {
+ if (!grievanceText.trim() && !attachedFile) return;
+
+ setLoading(true);
+ setError(null);
+ try {
+ const result = await analyzeIssueApi(
+ grievanceText || `Document Analysis: ${attachedFile?.name}`,
+ selectedCity,
+ attachedFile || undefined
+ );
+ onLaunchCase(result, grievanceText || attachedFile?.name || 'New Filing');
+ onClose();
+ } catch (err: any) {
+ setError(err?.message || 'Processing failed. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Create Legal Notice / RTI
+
+
Jurisdiction: {selectedCity}
+
+
+ close
+
+
+
+ {/* Input 1: Model Selector */}
+
+
+ 1. Select Model / Framework
+
+
+ {[
+ { id: 'rti', label: 'RTI Request', icon: 'description' },
+ { id: 'consumer', label: 'Consumer', icon: 'shopping_bag' },
+ { id: 'tenancy', label: 'Tenancy / Salary', icon: 'home_work' },
+ ].map((m) => (
+ setSelectedModel(m.id)}
+ className={`p-2.5 rounded-xl border flex flex-col items-center gap-1 text-[12.5px] font-semibold transition-all cursor-pointer ${
+ selectedModel === m.id
+ ? 'border-[#006c4a] bg-[#ECFDF5] text-[#006c4a]'
+ : 'border-[#CBD5E1] text-[#475569] hover:bg-[#F8FAFC]'
+ }`}
+ >
+ {m.icon}
+ {m.label}
+
+ ))}
+
+
+
+ {/* Input 2: Grievance Input */}
+
+
+ 2. Describe Your Grievance / Requirements
+
+
+
+ {/* Input 3: PDF Attachment */}
+
+
+ 3. Attach PDF Document (Optional)
+
+
+ setAttachedFile(e.target.files?.[0] || null)}
+ className="hidden"
+ />
+
+ upload_file
+ {attachedFile ? attachedFile.name : 'Select PDF File'}
+
+ {attachedFile && (
+ setAttachedFile(null)}
+ className="text-[#DC2626] text-[12px] font-semibold hover:underline cursor-pointer"
+ >
+ Remove
+
+ )}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Submit */}
+
+
+ Cancel
+
+
+ {loading ? (
+ <>
+ sync
+ Generating Draft...
+ >
+ ) : (
+ <>
+ Generate Filing
+ arrow_forward
+ >
+ )}
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/Header.tsx b/Frontend/src/components/Header.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4ad640157bc6849c27206fe16c573df2b9b765b8
--- /dev/null
+++ b/Frontend/src/components/Header.tsx
@@ -0,0 +1,357 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useAuth } from '../context/AuthContext';
+import { AuthModal } from './AuthModal';
+
+interface HeaderProps {
+ currentView: 'home' | 'workspace' | 'my-filings';
+ onNavigate: (view: 'home' | 'workspace' | 'my-filings') => void;
+ selectedCity: string;
+ onSelectCity: (city: string) => void;
+ unreadNotifications?: number;
+ filingsCount?: number;
+}
+
+export const CITIES = [
+ 'Delhi (NCR)',
+ 'Bengaluru',
+ 'Mumbai',
+ 'Chennai',
+ 'Hyderabad',
+ 'Kolkata',
+ 'Pune',
+ 'Ahmedabad',
+ 'Jaipur',
+ 'Chandigarh',
+];
+
+export const Header: React.FC = ({
+ currentView,
+ onNavigate,
+ selectedCity,
+ onSelectCity,
+ unreadNotifications = 2,
+ filingsCount = 0,
+}) => {
+ const { user, loading, signOut, sendVerificationEmail, reloadUser } = useAuth();
+ const [showCityMenu, setShowCityMenu] = useState(false);
+ const [showNotificationMenu, setShowNotificationMenu] = useState(false);
+ const [showProfileMenu, setShowProfileMenu] = useState(false);
+ const [showAuthModal, setShowAuthModal] = useState(false);
+ const [authWorking, setAuthWorking] = useState(false);
+ const [verifSent, setVerifSent] = useState(false);
+ const [verifError, setVerifError] = useState(null);
+
+ const cityMenuRef = useRef(null);
+ const notificationMenuRef = useRef(null);
+ const profileMenuRef = useRef(null);
+
+ const handleSendVerification = async () => {
+ setVerifError(null);
+ setVerifSent(false);
+ try {
+ await sendVerificationEmail();
+ setVerifSent(true);
+ setTimeout(() => setVerifSent(false), 5000);
+ } catch (err: any) {
+ setVerifError(err?.message || 'Could not send verification email.');
+ }
+ };
+
+ const handleCheckVerification = async () => {
+ try {
+ await reloadUser();
+ } catch (err) {
+ console.warn('Could not reload user status:', err);
+ }
+ };
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ const target = event.target as Node;
+ if (cityMenuRef.current && !cityMenuRef.current.contains(target)) {
+ setShowCityMenu(false);
+ }
+ if (notificationMenuRef.current && !notificationMenuRef.current.contains(target)) {
+ setShowNotificationMenu(false);
+ }
+ if (profileMenuRef.current && !profileMenuRef.current.contains(target)) {
+ setShowProfileMenu(false);
+ }
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setShowCityMenu(false);
+ setShowNotificationMenu(false);
+ setShowProfileMenu(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ window.addEventListener('keydown', handleKeyDown);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ window.removeEventListener('keydown', handleKeyDown);
+ };
+ }, []);
+
+ const handleSignOut = async () => {
+ setAuthWorking(true);
+ try {
+ await signOut();
+ setShowProfileMenu(false);
+ } catch (err) {
+ console.error('Sign-out error:', err);
+ } finally {
+ setAuthWorking(false);
+ }
+ };
+
+ return (
+ <>
+
+
+ setShowAuthModal(false)}
+ />
+ >
+ );
+};
diff --git a/Frontend/src/components/LegalInfoModal.tsx b/Frontend/src/components/LegalInfoModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a1885b5252b55ea60da3127efaa5b9c8c04b2bdc
--- /dev/null
+++ b/Frontend/src/components/LegalInfoModal.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+
+interface LegalInfoModalProps {
+ type: 'disclaimer' | 'accessibility' | 'rulebooks' | 'privacy';
+ onClose: () => void;
+}
+
+export const LegalInfoModal: React.FC = ({ type, onClose }) => {
+ const contentMap = {
+ disclaimer: {
+ title: 'Legal Disclaimers & Civic Tech Notice',
+ subtitle: 'Information clarity for citizens and legal self-representation',
+ body: (
+
+
+ CaseLoop is a civic-technology platform designed to facilitate automated drafting of statutory legal notices, public records requests, and consumer dispute frameworks.
+
+
+ Notice: CaseLoop does not provide legal advice and is not a law firm. Using CaseLoop does not establish an attorney-client relationship. If you are facing complex civil litigation or criminal charges, consider consulting an accredited legal aid clinic or licensed attorney.
+
+
+ All generated documents are compiled according to public statutory statutes (such as Sunshine Laws, RTI Act 2005, and the Consumer Protection Act). Users retain complete control over fact accuracy and submission.
+
+
+ ),
+ },
+ accessibility: {
+ title: 'Accessibility Statement (WCAG AAA)',
+ subtitle: 'Engineered for high readability in high-stress citizen situations',
+ body: (
+
+
+ CaseLoop adheres strictly to Web Content Accessibility Guidelines (WCAG 2.1) Level AAA standards for color contrast, screen reader compatibility, and keyboard navigation.
+
+
+ Minimum 7:1 contrast ratio on all primary reading text surfaces.
+ Full ARIA label landmarks and keyboard shortcut traversal (Tab, Enter, Escape).
+ Zero disruptive animations; motion respects prefers-reduced-motion.
+ Plain English translation layers for dense bureaucratic legalese.
+
+
+ ),
+ },
+ rulebooks: {
+ title: 'Civic Rulebooks & Statutory Sources',
+ subtitle: 'Open-source statutory codifications powering automated drafting',
+ body: (
+
+
+ Each CaseLoop Rulebook represents a verified legal procedural tree covering statutory deadlines, mandatory elements of claim, and public record officer routing.
+
+
+ Rulebooks are audited quarterly against municipal ordinance updates, appellate court precedents, and government department organizational charts.
+
+
+ ),
+ },
+ privacy: {
+ title: 'Privacy & Automated Redaction Policy',
+ subtitle: 'Zero-knowledge PII vault and client-side redaction',
+ body: (
+
+
+ Your privacy is our primary engineering requirement. Personal Identifiable Information (PII) including physical addresses, personal email addresses, phone numbers, and financial instruments are auto-redacted before any public drafting or export.
+
+
+ Zero Telemetry: Case filings and citizen grievance descriptions are stored in your secure local session and are never sold or shared with data brokers.
+
+
+ ),
+ },
+ };
+
+ const current = contentMap[type] || contentMap.disclaimer;
+
+ return (
+
+
+
+
+
+ {current.title}
+
+
{current.subtitle}
+
+
+ close
+
+
+
+
{current.body}
+
+
+
+ Understood
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/MyFilingsDashboard.tsx b/Frontend/src/components/MyFilingsDashboard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3871941a1972ca3834d09994158f671a8f1ae1f3
--- /dev/null
+++ b/Frontend/src/components/MyFilingsDashboard.tsx
@@ -0,0 +1,408 @@
+import React, { useState } from 'react';
+import { LegalCase } from '../types';
+import { useAuth } from '../context/AuthContext';
+
+interface MyFilingsDashboardProps {
+ cases: LegalCase[];
+ onSelectCase: (caseId: string) => void;
+ onNewFiling: () => void;
+ onDeleteCase?: (caseId: string) => void;
+ onDuplicateCase?: (legalCase: LegalCase) => void;
+ onDownloadPDF: (legalCase: LegalCase) => void;
+ onShareCase: (legalCase: LegalCase) => void;
+ onOpenQuickAction: (action: 'appeal' | 'history' | 'rulebooks') => void;
+}
+
+export const MyFilingsDashboard: React.FC = ({
+ cases,
+ onSelectCase,
+ onNewFiling,
+ onDeleteCase,
+ onDuplicateCase,
+ onDownloadPDF,
+ onShareCase,
+ onOpenQuickAction,
+}) => {
+ const { user, signIn } = useAuth();
+ const [filterCategory, setFilterCategory] = useState('all');
+ const [filterStatus, setFilterStatus] = useState('all');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [deleteConfirmId, setDeleteConfirmId] = useState(null);
+
+ const activeCasesCount = cases.length;
+ const stressTestedCount = cases.filter((c) => c.score >= 85 || c.status === 'stress_tested').length;
+ const upcomingDeadlinesCount = cases.filter((c) => c.statusType === 'urgent' || c.daysRemaining <= 7).length;
+
+ const filteredCases = cases.filter((c) => {
+ const matchesCategory =
+ filterCategory === 'all' ||
+ c.category.toLowerCase().includes(filterCategory.toLowerCase()) ||
+ c.rulebookId.toLowerCase().includes(filterCategory.toLowerCase());
+
+ const matchesStatus =
+ filterStatus === 'all' ||
+ (filterStatus === 'urgent' && (c.statusType === 'urgent' || c.daysRemaining <= 7)) ||
+ (filterStatus === 'drafting' && c.status === 'drafting') ||
+ (filterStatus === 'tested' && c.score >= 85);
+
+ const matchesSearch =
+ c.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ c.caseNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (c.statute && c.statute.toLowerCase().includes(searchQuery.toLowerCase()));
+
+ return matchesCategory && matchesStatus && matchesSearch;
+ });
+
+ return (
+
+ {/* Header Section */}
+
+
+
+ {user ? (
+
+
+ Cloud Synced (Firebase)
+
+ ) : (
+ signIn()}
+ className="group inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-slate-100 hover:bg-slate-200 text-[#0F172A] text-[12px] font-semibold transition-all duration-150 cursor-pointer border border-slate-200"
+ >
+ account_circle
+ Sign in to save filings across devices
+ chevron_right
+
+ )}
+
+
+ My Filings
+
+
+ Your saved legal notices, deadlines, and letters.
+
+
+
+
+
+ add_circle
+ New Case Filing
+
+
+
+
+ {/* Bento Grid Dashboard */}
+
+ {/* Summary Stats (12 cols) */}
+
+ {/* Active Cases */}
+
+
+ folder_open
+
+
+
Total Active Cases
+
{activeCasesCount}
+
+
+
+ {/* Stress-Tested */}
+
+
+ health_and_safety
+
+
+
High-Viability Filings
+
{stressTestedCount}
+
+
+
+ {/* Upcoming Deadlines */}
+
+
+ timer
+
+
+
Upcoming Deadlines
+
{upcomingDeadlinesCount}
+
+
+
+
+ {/* Active Filings List (8 cols) */}
+
+
+
+ Saved Case Filings ({filteredCases.length})
+
+
+ {/* Quick search/filter within active filings */}
+
+ setSearchQuery(e.target.value)}
+ className="text-[12px] bg-white border border-[#CBD5E1] rounded-lg px-2.5 py-1 text-[#1E293B] focus:border-[#0F172A] outline-none"
+ />
+ setFilterStatus(e.target.value)}
+ className="text-[12px] bg-white border border-[#CBD5E1] rounded-lg px-2.5 py-1 text-[#475569] cursor-pointer"
+ >
+ All Statuses
+ In Drafting
+ High Viability (≥85%)
+ Urgent Clocks (≤7d)
+
+
+
+
+
+ {filteredCases.length === 0 ? (
+
+
+ description
+
+
No Filings Found
+
+ {searchQuery || filterStatus !== 'all'
+ ? 'No cases match your active search filters. Try clearing your search.'
+ : 'Start your first case filing or analyze a civic grievance on the home screen.'}
+
+
+ Create New Filing
+
+
+ ) : (
+ filteredCases.map((legalCase) => {
+ const isUrgent = legalCase.statusType === 'urgent' || legalCase.daysRemaining <= 7;
+ const isSafe = legalCase.score >= 85 || legalCase.statusType === 'success';
+
+ return (
+
+ {/* Color Accent Indicator Left Bar */}
+
+
+ {/* Header Row */}
+
+
+
+ gavel
+ {legalCase.category}
+
+
+
+ {legalCase.caseNumber}
+
+
+ {isUrgent && (
+
+ {legalCase.statusBadge || 'Urgent'}
+
+ )}
+
+ {isSafe && (
+
+ check_circle
+ Score: {legalCase.score}%
+
+ )}
+
+
+ {/* Statutory Deadline Pill */}
+
+ schedule
+ {legalCase.daysRemaining} Days Window
+
+
+
+ {/* Case Content */}
+
+
+ {legalCase.title}
+
+
+ {legalCase.description || legalCase.requestScope}
+
+ {legalCase.statute && (
+
+ Statutory Hook: {legalCase.statute}
+
+ )}
+
+
+ {/* Action Buttons */}
+
+
+ onSelectCase(legalCase.id)}
+ className="bg-[#006c4a] hover:bg-[#005137] text-white text-[13px] font-semibold px-4 py-2 rounded-lg transition-colors cursor-pointer flex items-center gap-1"
+ >
+ edit
+ Open Workspace
+
+
+ onDownloadPDF(legalCase)}
+ className="bg-[#F1F5F9] hover:bg-[#E2E8F0] text-[#0F172A] text-[13px] font-medium px-3.5 py-2 rounded-lg transition-colors flex items-center gap-1 cursor-pointer border border-[#CBD5E1]"
+ >
+ download
+ PDF
+
+
+ {onDuplicateCase && (
+ onDuplicateCase(legalCase)}
+ className="bg-[#F1F5F9] hover:bg-[#E2E8F0] text-[#475569] text-[13px] font-medium px-3 py-2 rounded-lg transition-colors flex items-center gap-1 cursor-pointer"
+ title="Duplicate Case Filing"
+ >
+ content_copy
+ Duplicate
+
+ )}
+
+
+
+
onShareCase(legalCase)}
+ className="bg-[#F1F5F9] hover:bg-[#E2E8F0] text-[#475569] text-[13px] font-medium px-3 py-2 rounded-lg transition-colors flex items-center gap-1 cursor-pointer"
+ title="Share filing"
+ >
+ share
+ Share
+
+
+ {onDeleteCase && (
+ deleteConfirmId === legalCase.id ? (
+
+ {
+ onDeleteCase(legalCase.id);
+ setDeleteConfirmId(null);
+ }}
+ className="bg-[#DC2626] text-white text-[11px] font-bold px-2 py-1 rounded cursor-pointer"
+ >
+ Confirm Delete
+
+ setDeleteConfirmId(null)}
+ className="text-[#64748B] text-[11px] px-1.5 py-1 hover:text-[#0F172A] cursor-pointer"
+ >
+ Cancel
+
+
+ ) : (
+
setDeleteConfirmId(legalCase.id)}
+ className="text-[#94A3B8] hover:text-[#DC2626] p-2 rounded-lg hover:bg-[#FEF2F2] transition-colors cursor-pointer"
+ title="Delete Filing"
+ >
+ delete
+
+ )
+ )}
+
+
+
+ );
+ })
+ )}
+
+
+
+ {/* Side Panel (4 cols) */}
+
+
+ Civic Legal Utilities
+
+
+
+
+ {/* Action 1: Track Appeal Status */}
+ onOpenQuickAction('appeal')}
+ className="py-3 border-b border-[#F1F5F9] last:border-0 flex items-center justify-between group cursor-pointer hover:bg-[#F8FAFC] transition-colors rounded-lg px-2"
+ >
+
+
+ track_changes
+
+
Track Statutory Appeal Status
+
+
+ chevron_right
+
+
+
+ {/* Action 2: Review Revision History */}
+ onOpenQuickAction('history')}
+ className="py-3 border-b border-[#F1F5F9] last:border-0 flex items-center justify-between group cursor-pointer hover:bg-[#F8FAFC] transition-colors rounded-lg px-2"
+ >
+
+
+ history
+
+
Revision History & Audits
+
+
+ chevron_right
+
+
+
+ {/* Action 3: Access Rulebooks */}
+ onOpenQuickAction('rulebooks')}
+ className="py-3 border-b border-[#F1F5F9] last:border-0 flex items-center justify-between group cursor-pointer hover:bg-[#F8FAFC] transition-colors rounded-lg px-2"
+ >
+
+
+ gavel
+
+
Statutory Rulebooks Catalog
+
+
+ chevron_right
+
+
+
+
+
+ {/* Real-time Cloud Status Banner */}
+
+
+ cloud_sync
+
+
+
+ Cloud Persistence
+
+
+
+
+
Firebase Firestore Real-Time Active
+
+
+
+ Every draft revision, extracted fact, PII mask, and stress-test audit is automatically saved to your cloud profile.
+
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/PDFExportModal.tsx b/Frontend/src/components/PDFExportModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f9c455db512ed89bd9a207725a5b5091e5d8ca78
--- /dev/null
+++ b/Frontend/src/components/PDFExportModal.tsx
@@ -0,0 +1,193 @@
+import React, { useState } from 'react';
+import { LegalCase } from '../types';
+import { exportLegalCaseToPDF } from '../services/pdfService';
+
+interface PDFExportModalProps {
+ legalCase: LegalCase;
+ onClose: () => void;
+}
+
+export const PDFExportModal: React.FC = ({ legalCase, onClose }) => {
+ const [downloading, setDownloading] = useState(false);
+
+ const handleDownloadDirectPDF = () => {
+ setDownloading(true);
+ try {
+ exportLegalCaseToPDF(legalCase);
+ } catch (err) {
+ console.error('Error generating PDF:', err);
+ } finally {
+ setDownloading(false);
+ }
+ };
+
+ const handlePrint = () => {
+ window.print();
+ };
+
+ return (
+
+
e.stopPropagation()}
+ className="bg-white rounded-2xl max-w-3xl w-full max-h-[90vh] flex flex-col shadow-2xl border border-[#E2E8F0] overflow-hidden cursor-default"
+ >
+ {/* Modal Top Bar */}
+
+
+
+ picture_as_pdf
+
+
+
+ Download Official Legal Notice
+
+
+ Compliant with Statutory Filing Standards
+
+
+
+
+
+
+
+ {downloading ? 'sync' : 'download'}
+
+ {downloading ? 'Generating...' : 'Download PDF'}
+
+
+
+ print
+ Print
+
+
+
+ close
+
+
+
+
+ {/* Document Preview Box with Print ID Container */}
+
+
+ {/* Header Stamps */}
+
+
+
+ Formal Legal Notice & Records Request
+
+
+ Case Reference: {legalCase.caseNumber} • Electronic Vault Verified
+
+
+
+
{legalCase.filedDate || 'Current Filing'}
+
+ Statutory {legalCase.daysRemaining || 30}-Day Clock
+
+
+
+
+ {/* Recipient */}
+
+
To: {legalCase.officer?.name || 'Authorized Officer'}
+
{legalCase.officer?.title || 'Designated Officer'}
+
{legalCase.officer?.department || 'Grievance Redressal Division'}
+
{legalCase.officer?.jurisdiction || 'Local Jurisdiction'}
+
+
+ {/* RE Line */}
+
+ RE: Formal Notice regarding {legalCase.title} pursuant to {legalCase.statute}
+
+
+ {/* Body */}
+
+ {legalCase.description && legalCase.description.trim().length > 30 ? (
+
+ {legalCase.description}
+
+ ) : (
+ <>
+
Dear {legalCase.officer?.name || 'Public Officer'},
+
+ I am formally submitting this legal notice regarding the matter detailed below:
+
+
+
+
1. Particulars & Facts:
+
+ {(legalCase.facts || []).map((fact) => (
+
+ {fact.label}: {fact.value}
+
+ ))}
+
+
+
+
+
2. Requested Relief / Records:
+
{legalCase.requestScope || 'Full resolution and statutory compliance.'}
+
+ >
+ )}
+
+ {legalCase.hasHardshipClause && (
+
+ Statutory Fee Waiver: The requester hereby requests that all search and duplication fees be waived as disclosure is in the public interest.
+
+ )}
+
+
+ Pursuant to statutory mandates, should any portions of these records contain exempt material, you are required to segregate and redact only the exempt material and release all non-exempt portions without delay.
+
+
+
+ {/* Sign-off */}
+
+
+
Respectfully submitted,
+
+ {legalCase.autoRedactEnabled ? '[REDACTED REQUESTER]' : 'Authorized Citizen / Requester'}
+
+
CaseLoop Verified Electronic Identity
+
+
+
+
Verified Case: {legalCase.caseNumber}
+
Statutory Notice v1
+
+
+
+
+
+ {/* Footer */}
+
+ Includes cryptographic timestamp & auto-redacted personal data.
+
+ Close
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/RevisionHistoryModal.tsx b/Frontend/src/components/RevisionHistoryModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..399eb133039fbd6f68811f17a9e6b6829244673a
--- /dev/null
+++ b/Frontend/src/components/RevisionHistoryModal.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+
+interface RevisionHistoryModalProps {
+ onClose: () => void;
+}
+
+export const RevisionHistoryModal: React.FC = ({ onClose }) => {
+ const revisions = [
+ {
+ id: 'rev-3',
+ version: 'v1.3 (Current)',
+ time: '35 minutes ago',
+ author: 'Automated Stress-Tester',
+ change: 'Injected Public Interest Hardship Waiver Clause to mitigate fee stall tactics.',
+ scoreChange: '+12% (84% -> 96%)',
+ badge: 'Optimized',
+ },
+ {
+ id: 'rev-2',
+ version: 'v1.2',
+ time: '2 hours ago',
+ author: 'Privacy Redaction Engine',
+ change: 'Masked Street Address (124 Main St) and Email (j.doe@email.com) in public draft.',
+ scoreChange: 'PII Protected',
+ badge: 'Secured',
+ },
+ {
+ id: 'rev-1',
+ version: 'v1.0',
+ time: 'Oct 24, 2024 • 09:30 AM',
+ author: 'Citizen Intake',
+ change: 'Initial grievance ingestion and statutory framework mapping to Sunshine Law § 119.07.',
+ scoreChange: 'Initial Base',
+ badge: 'Created',
+ },
+ ];
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ history
+
+ Revision History & Audit Log
+
+
+
+ Cryptographic integrity log for legal drafting and stress test iterations
+
+
+
+ close
+
+
+
+ {/* Content */}
+
+ {revisions.map((rev) => (
+
+
+
+
+ {rev.version}
+
+
+ {rev.time}
+
+
+
+
+ {rev.scoreChange}
+
+
+
+
{rev.change}
+
+
+ person
+ Triggered by: {rev.author}
+
+
+ ))}
+
+
+ {/* Footer */}
+
+
+ Done
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/RtiAuditorModal.tsx b/Frontend/src/components/RtiAuditorModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..87ae7c7c81b3028db85867e64a941ef51804134a
--- /dev/null
+++ b/Frontend/src/components/RtiAuditorModal.tsx
@@ -0,0 +1,434 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { auditRtiApi, analyzeDocumentApi, RtiAuditResponse } from '../services/api';
+
+interface RtiAuditorModalProps {
+ selectedCity: string;
+ onClose: () => void;
+ onLaunchRtiCase?: (optimizedDraft: string, title: string) => void;
+}
+
+const SAMPLE_RTI_DRAFTS = [
+ {
+ title: 'Ward Road Resurfacing Tender & Quality Audit',
+ text: 'Why is the road work in Ward 150 delayed? Who is the contractor and why did it develop potholes within 3 months? When will it be repaired?',
+ },
+ {
+ title: 'Building Plan Sanction File Movement',
+ text: 'Please inform me why my building plan sanction application #BBMP/LP/2024/890 is still pending after 60 days. Which officer is sitting on the file?',
+ },
+ {
+ title: 'Police Action-Taken Report on NCR',
+ text: 'What action has been taken on my complaint filed on 10th August? Why has the FIR not been registered yet and who investigated it?',
+ },
+];
+
+export const RtiAuditorModal: React.FC = ({
+ selectedCity,
+ onClose,
+ onLaunchRtiCase,
+}) => {
+ const [rtiText, setRtiText] = useState(SAMPLE_RTI_DRAFTS[0].text);
+ const [auditing, setAuditing] = useState(false);
+ const [auditResult, setAuditResult] = useState(null);
+ const [auditError, setAuditError] = useState(null);
+ const [copiedDraft, setCopiedDraft] = useState(false);
+
+ // Document / PDF Upload State
+ const [uploadedFileName, setUploadedFileName] = useState(null);
+ const [isProcessingFile, setIsProcessingFile] = useState(false);
+ const fileInputRef = useRef(null);
+
+ // Close on Escape key press
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ onClose();
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [onClose]);
+
+ const handleFileUpload = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ setUploadedFileName(file.name);
+ setIsProcessingFile(true);
+ setAuditError(null);
+
+ const reader = new FileReader();
+ reader.onload = async () => {
+ const result = reader.result as string;
+ const base64Data = result.split(',')[1] || result;
+
+ try {
+ setAuditing(true);
+ // Extract text & diagnosis using document analysis
+ const docAnalysis = await analyzeDocumentApi({
+ fileBase64: base64Data,
+ mimeType: file.type || 'application/pdf',
+ fileName: file.name,
+ jurisdiction: selectedCity,
+ });
+
+ if (docAnalysis) {
+ const extractedGrievance = `${docAnalysis.title}\n${docAnalysis.legalDiagnosis || ''}\n${docAnalysis.requestScope || ''}`.trim();
+ setRtiText(extractedGrievance);
+ // Run RTI quality audit on the extracted text
+ const audit = await auditRtiApi(extractedGrievance, selectedCity);
+ setAuditResult(audit);
+ }
+ } catch (err: any) {
+ console.error('PDF extraction error in RTI auditor:', err);
+ setAuditError(err?.message || 'Could not extract text from document. Please copy-paste text instead.');
+ } finally {
+ setIsProcessingFile(false);
+ setAuditing(false);
+ }
+ };
+ reader.onerror = () => {
+ setIsProcessingFile(false);
+ setAuditError('Failed to read the uploaded document.');
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const handleRunAudit = async (customText?: string) => {
+ const text = customText !== undefined ? customText : rtiText;
+ if (!text.trim()) return;
+
+ setAuditing(true);
+ setAuditError(null);
+ try {
+ const res = await auditRtiApi(text, selectedCity);
+ setAuditResult(res);
+ } catch (err: any) {
+ console.error('RTI Audit error:', err);
+ setAuditError(err?.message || 'Failed to complete RTI audit. Please retry.');
+ } finally {
+ setAuditing(false);
+ }
+ };
+
+ const handleCopy = (text: string) => {
+ navigator.clipboard.writeText(text);
+ setCopiedDraft(true);
+ setTimeout(() => setCopiedDraft(false), 2500);
+ };
+
+ return (
+ {
+ if (e.target === e.currentTarget) {
+ onClose();
+ }
+ }}
+ className="fixed inset-0 bg-black/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-in fade-in duration-200 cursor-pointer"
+ >
+
e.stopPropagation()}
+ className="bg-white rounded-2xl max-w-4xl w-full max-h-[90vh] flex flex-col shadow-2xl border border-[#CBD5E1] overflow-hidden cursor-default"
+ >
+ {/* Header */}
+
+
+
+ verified
+
+
+
+
+ RTI Strength & Quality Auditor
+
+
+ RTI Act 2005 Compliance
+
+
+
+ Audit draft questions or upload rejection letters/PDFs to get a reject-proof statutory RTI draft
+
+
+
+
+
+ close
+
+
+
+ {/* Body */}
+
+ {/* Input & PDF Upload Section */}
+
+ {/* PDF Upload Banner */}
+
+
+
+ upload_file
+
+
+
+ {uploadedFileName ? `Loaded: ${uploadedFileName}` : 'Have an RTI Draft or Govt Rejection Order in PDF?'}
+
+
+ {uploadedFileName
+ ? 'Document extracted. Audit results generated below.'
+ : 'Upload PIO reply, Section 8 rejection letter, or application draft (PDF / Image)'}
+
+
+
+
+
+
+ fileInputRef.current?.click()}
+ disabled={isProcessingFile || auditing}
+ className="bg-white hover:bg-[#F1F5F9] text-[#0F172A] border border-[#CBD5E1] font-semibold text-[12.5px] px-3.5 py-2 rounded-lg transition-colors cursor-pointer flex items-center gap-1.5 shadow-xs"
+ >
+
+ {isProcessingFile ? 'sync' : 'attach_file'}
+
+ {uploadedFileName ? 'Upload Different PDF' : 'Upload PDF / Doc'}
+
+
+
+
+
+
+ Or Type / Paste Your RTI Questions:
+
+
+ Try sample:
+ {SAMPLE_RTI_DRAFTS.map((sample, idx) => (
+ {
+ setRtiText(sample.text);
+ handleRunAudit(sample.text);
+ }}
+ className="text-[#006c4a] hover:underline font-semibold cursor-pointer"
+ >
+ #{idx + 1}
+
+ ))}
+
+
+
+
+
+ {auditError && (
+
+ error
+ {auditError}
+
+ )}
+
+ {/* Audit Results Dashboard */}
+ {auditResult && (
+
+ {/* Score & Verdict Banner */}
+
+ {/* Score Dial */}
+
+
+ RTI Strength Score
+
+
+ {auditResult.score}
+ /100
+
+
= 85
+ ? 'bg-[#DCFCE7] text-[#166534]'
+ : auditResult.score >= 70
+ ? 'bg-[#FEF3C7] text-[#92400E]'
+ : 'bg-[#FEE2E2] text-[#991B1B]'
+ }`}
+ >
+ {auditResult.grade}
+
+
+
+ {/* Verdict Summary */}
+
+
+
+ Statutory Audit Verdict
+
+
+ Est. Success Rate: {auditResult.estimatedSuccessRate || 92}%
+
+
+
+ {auditResult.verdictSummary}
+
+ {auditResult.exemptionsRiskAnalysis && (
+
+ Exemption Risk:
+ {auditResult.exemptionsRiskAnalysis}
+
+ )}
+
+
+
+ {/* 7-Point Compliance Checklist */}
+
+
+ checklist
+ 7-Point Statutory Compliance Checklist
+
+
+
+ {auditResult.checklist.map((item, idx) => (
+
+
+
+
+ {item.status === 'pass'
+ ? 'check_circle'
+ : item.status === 'warning'
+ ? 'warning'
+ : 'cancel'}
+
+ {item.item}
+
+
+ {item.rule}
+
+
+
{item.finding}
+ {item.statutoryFix && item.status !== 'pass' && (
+
+ Fix: {item.statutoryFix}
+
+ )}
+
+ ))}
+
+
+
+ {/* Optimized Ready-to-Submit RTI Draft */}
+ {auditResult.optimizedRtiDraft && (
+
+
+
+
+ auto_awesome
+
+
+ Optimized & Bulletproof RTI Application
+
+
+
+
+ handleCopy(auditResult.optimizedRtiDraft)}
+ className="bg-[#F1F5F9] hover:bg-[#E2E8F0] text-[#0F172A] font-semibold text-[12.5px] px-3 py-1.5 rounded-lg flex items-center gap-1 transition-colors cursor-pointer border border-[#CBD5E1]"
+ >
+
+ {copiedDraft ? 'check' : 'content_copy'}
+
+ {copiedDraft ? 'Copied' : 'Copy Application'}
+
+
+ {onLaunchRtiCase && (
+ {
+ onLaunchRtiCase(
+ auditResult.optimizedRtiDraft,
+ `RTI: ${rtiText.slice(0, 40)}...`
+ );
+ onClose();
+ }}
+ className="bg-[#006c4a] hover:bg-[#005137] text-white font-bold text-[12.5px] px-4 py-1.5 rounded-lg flex items-center gap-1 transition-all cursor-pointer shadow-xs hover:scale-[1.02]"
+ >
+ Open in Workspace
+ arrow_forward
+
+ )}
+
+
+
+
+ {auditResult.optimizedRtiDraft}
+
+
+ )}
+
+ )}
+
+
+ {/* Footer */}
+
+
+ Section 6(1) Right to Information Act 2005
+
+
+ Close
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/RulebookSelector.tsx b/Frontend/src/components/RulebookSelector.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..96b4efbd8a1d53de8e2e815ac3d88e7868494690
--- /dev/null
+++ b/Frontend/src/components/RulebookSelector.tsx
@@ -0,0 +1,717 @@
+import React, { useState, useRef } from 'react';
+import { Rulebook } from '../types';
+import { FoldText } from './animations/FoldText';
+import { RotatingText } from './animations/RotatingText';
+import { TextType } from './animations/TextType';
+import { analyzeIssueApi, AIAnalysisResponse } from '../services/api';
+
+interface RulebookSelectorProps {
+ rulebooks: Rulebook[];
+ selectedCity?: string;
+ onSelectRulebook: (rulebookId: string, initialPrompt?: string) => void;
+ onLaunchAICase?: (aiData: AIAnalysisResponse, rawText: string) => void;
+ onSaveAICaseToCloud?: (aiData: AIAnalysisResponse, rawText: string) => Promise;
+ onViewAllRulebooks: () => void;
+ onAnalyzePrompt: (prompt: string) => void;
+ onOpenGuidedIntake?: (category?: string) => void;
+ onOpenRtiAuditor?: () => void;
+}
+
+const SAMPLE_SCENARIOS = [
+ 'Landlord refusing to refund ₹1,40,000 security deposit after clean handover',
+ 'Municipal corporation tender expenditure and quality audit for Ward 150 road work',
+ 'E-commerce seller delivered defective laptop and refused refund of ₹64,990',
+ 'Employer withholding 2 months salary and ₹1,80,000 F&F settlement after resignation',
+ 'Bank refusing chargeback for ₹50,000 unauthorized credit card transaction',
+];
+
+export const RulebookSelector: React.FC = ({
+ rulebooks,
+ selectedCity = 'Delhi (NCR)',
+ onSelectRulebook,
+ onLaunchAICase,
+ onSaveAICaseToCloud,
+ onViewAllRulebooks,
+ onAnalyzePrompt,
+ onOpenGuidedIntake,
+ onOpenRtiAuditor,
+}) => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [isInputFocused, setIsInputFocused] = useState(false);
+ const [selectedPdf, setSelectedPdf] = useState(null);
+ const [analyzing, setAnalyzing] = useState(false);
+ const [analysisError, setAnalysisError] = useState(null);
+ const [aiAnalysis, setAiAnalysis] = useState(null);
+ const [savedToCloudFeedback, setSavedToCloudFeedback] = useState(false);
+ const [copiedBrief, setCopiedBrief] = useState(false);
+ const [copiedRtiDraft, setCopiedRtiDraft] = useState(false);
+
+ const fileInputRef = useRef(null);
+
+ const handlePdfUpload = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
+ setSelectedPdf(file);
+ if (!searchQuery.trim()) {
+ setSearchQuery(`Document Attached: ${file.name}`);
+ }
+ } else {
+ alert('Please select a valid PDF file.');
+ }
+ }
+ };
+
+ const handleRemovePdf = () => {
+ setSelectedPdf(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ if (searchQuery.startsWith('Document Attached:')) {
+ setSearchQuery('');
+ }
+ };
+
+ const handleAnalyze = async (textToAnalyze?: string) => {
+ let text = (textToAnalyze ?? searchQuery).trim();
+ if (!text && !selectedPdf) {
+ text = SAMPLE_SCENARIOS[0];
+ setSearchQuery(text);
+ } else if (!text && selectedPdf) {
+ text = `Analyze attached document: ${selectedPdf.name}`;
+ setSearchQuery(text);
+ }
+
+ setAnalyzing(true);
+ setAnalysisError(null);
+ setAiAnalysis(null);
+ setSavedToCloudFeedback(false);
+
+ try {
+ const result = await analyzeIssueApi(text, selectedCity, selectedPdf || undefined);
+ setAiAnalysis(result);
+ } catch (err: any) {
+ console.error('Error analyzing grievance:', err);
+ setAnalysisError(err?.message || 'Failed to reach AI analysis engine. Please try again.');
+ } finally {
+ setAnalyzing(false);
+ }
+ };
+
+ const handleCopyBrief = () => {
+ if (!aiAnalysis) return;
+ const briefText = `CASE BRIEF:
+Title: ${aiAnalysis.title}
+Statute: ${aiAnalysis.statute}
+Category: ${aiAnalysis.category}
+Authority: ${aiAnalysis.officer.name} (${aiAnalysis.officer.title}, ${aiAnalysis.officer.department})
+Statutory Window: ${aiAnalysis.daysRemaining} days
+
+CIVIC RIGHTS:
+${aiAnalysis.civicRights?.map((r) => `• ${r}`).join('\n') || 'N/A'}
+
+${aiAnalysis.pdfSummary ? `DOCUMENT SUMMARY:\n${aiAnalysis.pdfSummary}\n\n` : ''}DRAFTED RTI APPLICATION:
+${aiAnalysis.draftedRti || aiAnalysis.formalLetter}
+
+LEGAL ASSESSMENT:
+${aiAnalysis.legalDiagnosis}`;
+
+ navigator.clipboard.writeText(briefText);
+ setCopiedBrief(true);
+ setTimeout(() => setCopiedBrief(false), 2500);
+ };
+
+ const handleCopyRtiDraft = () => {
+ if (!aiAnalysis) return;
+ const textToCopy = aiAnalysis.draftedRti || aiAnalysis.formalLetter || '';
+ navigator.clipboard.writeText(textToCopy);
+ setCopiedRtiDraft(true);
+ setTimeout(() => setCopiedRtiDraft(false), 2500);
+ };
+
+ const handleSaveToCloud = async () => {
+ if (!aiAnalysis || !onSaveAICaseToCloud) return;
+ try {
+ await onSaveAICaseToCloud(aiAnalysis, searchQuery || selectedPdf?.name || SAMPLE_SCENARIOS[0]);
+ setSavedToCloudFeedback(true);
+ setTimeout(() => setSavedToCloudFeedback(false), 3000);
+ } catch (err) {
+ console.error('Failed to save to cloud:', err);
+ }
+ };
+
+ return (
+
+ {/* Hero Section */}
+
+
+
+
+
+ {/* Subheading with Rotating Badge */}
+
+
Generate statutory legal notices and filings for
+
+
+
+
+
+ {/* Quick Action Badges */}
+
+ onOpenGuidedIntake && onOpenGuidedIntake('consumer')}
+ className="bg-[#006c4a] hover:bg-[#005137] text-white text-[13px] font-semibold px-4 py-2 rounded-xl flex items-center gap-1.5 transition-all shadow-xs cursor-pointer hover:scale-[1.02]"
+ >
+ add_circle
+ New Filing Draft
+
+
+ onOpenRtiAuditor && onOpenRtiAuditor()}
+ className="bg-white hover:bg-[#F8FAFC] border border-[#CBD5E1] hover:border-[#006c4a] text-[#0F172A] text-[13px] font-semibold px-4 py-2 rounded-xl flex items-center gap-1.5 transition-all shadow-2xs cursor-pointer hover:scale-[1.02]"
+ >
+ verified
+ Audit RTI Strength & PDFs
+
+
+
+ {/* Search & PDF Attachment Bar */}
+
+
+
+ search
+
+
+
setSearchQuery(e.target.value)}
+ onFocus={() => setIsInputFocused(true)}
+ onBlur={() => setIsInputFocused(false)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') handleAnalyze();
+ }}
+ placeholder={selectedPdf ? `Attached: ${selectedPdf.name}` : ''}
+ className="w-full pl-12 pr-48 py-4 rounded-xl bg-white border border-[#CBD5E1] focus:border-[#0F172A] focus:ring-2 focus:ring-[#0F172A]/10 text-[15px] sm:text-[16px] text-[#0F172A] shadow-sm transition-all duration-200"
+ />
+
+ {!searchQuery && !selectedPdf && !isInputFocused && (
+
+ e.g.
+
+
+ )}
+
+ {/* Clear button */}
+ {searchQuery && (
+
setSearchQuery('')}
+ className="absolute inset-y-0 right-44 sm:right-48 px-2 text-[#94A3B8] hover:text-[#0F172A] flex items-center cursor-pointer z-20"
+ title="Clear text"
+ >
+ cancel
+
+ )}
+
+ {/* Action Group: PDF Upload + Analyze */}
+
+
+
+ fileInputRef.current?.click()}
+ className={`h-full px-3 rounded-lg border text-[13px] font-semibold flex items-center gap-1 transition-all cursor-pointer ${
+ selectedPdf
+ ? 'bg-[#ECFDF5] border-[#86EFAC] text-[#006c4a]'
+ : 'bg-[#F8FAFC] hover:bg-[#F1F5F9] border-[#CBD5E1] text-[#475569]'
+ }`}
+ title="Upload RTI or Legal PDF document for analysis"
+ >
+
+ {selectedPdf ? 'task' : 'upload_file'}
+
+ {selectedPdf ? 'PDF Attached' : 'Attach PDF'}
+
+
+ handleAnalyze()}
+ disabled={analyzing}
+ className="h-full px-4 sm:px-5 bg-[#006c4a] hover:bg-[#005137] text-white rounded-lg font-semibold text-[14px] transition-all flex items-center justify-center gap-1.5 cursor-pointer shadow-sm disabled:opacity-75"
+ >
+ {analyzing ? (
+ <>
+ sync
+ Analyzing...
+ >
+ ) : (
+ <>
+ bolt
+ Analyze
+ >
+ )}
+
+
+
+
+ {/* Attached File Indicator */}
+ {selectedPdf && (
+
+
+ picture_as_pdf
+ {selectedPdf.name}
+ ({(selectedPdf.size / 1024).toFixed(1)} KB)
+
+
+ close
+ Remove
+
+
+ )}
+
+
+ {/* Quick Scenario Buttons */}
+
+ Quick Scenarios:
+ {SAMPLE_SCENARIOS.map((prompt, idx) => (
+ {
+ setSearchQuery(prompt);
+ handleAnalyze(prompt);
+ }}
+ className="bg-white hover:bg-[#F1F5F9] border border-[#E2E8F0] hover:border-[#CBD5E1] text-[#475569] px-3 py-1 rounded-full text-left transition-colors cursor-pointer shadow-2xs"
+ >
+ {prompt.length > 40 ? `${prompt.slice(0, 40)}...` : prompt}
+
+ ))}
+
+
+ {/* Error message */}
+ {analysisError && (
+
+
error
+
+
Analysis Notice
+
{analysisError}
+
handleAnalyze(SAMPLE_SCENARIOS[0])}
+ className="mt-2 bg-[#DC2626] text-white text-[12px] font-semibold px-3 py-1 rounded-md cursor-pointer hover:bg-[#B91C1C] transition-colors inline-flex items-center gap-1"
+ >
+ Retry with Standard Scenario
+
+
+
+ )}
+
+ {/* AI Analysis Results Dashboard */}
+ {aiAnalysis && (
+
+ {/* Header Strip */}
+
+
+
+ verified
+ {aiAnalysis.category}
+
+
+ Statute: {aiAnalysis.statute}
+
+
+
+
+
+ schedule
+ Statutory Clock: {aiAnalysis.daysRemaining} Days
+
+
+ thumb_up
+ Viability: {aiAnalysis.initialScore || 88}%
+
+
+
+
+ {/* Case Title & Legal Assessment */}
+
+
+ {aiAnalysis.title}
+
+
+ Legal Assessment:
+ {aiAnalysis.legalDiagnosis}
+
+
+
+ {/* 1. Civic Rights Box */}
+
+
+
+ balance
+ Your Civic Rights & Legal Standing
+
+
+ Statutory Entitlements
+
+
+
+
+ {aiAnalysis.civicRights && aiAnalysis.civicRights.length > 0 ? (
+ aiAnalysis.civicRights.map((right, index) => (
+
+
+ check_circle
+
+ {right}
+
+ ))
+ ) : (
+
+ You have the right to seek information, demand accountability, and obtain verified responses under statutory laws.
+
+ )}
+
+
+
+ {/* 2. Document Summary Box */}
+ {(aiAnalysis.pdfSummary || selectedPdf) && (
+
+
+
+ description
+ Document Summary (Plain English)
+
+
+ {selectedPdf ? selectedPdf.name : 'Analyzed Document'}
+
+
+
+ {aiAnalysis.pdfSummary || `Uploaded file "${selectedPdf?.name}" was analyzed and distilled into actionable civic rights and RTI queries.`}
+
+
+ )}
+
+ {/* 3. Drafted RTI Application Box */}
+
+
+
+ edit_document
+ Drafted RTI Application
+
+
+
+
+ {copiedRtiDraft ? 'done' : 'content_copy'}
+
+ {copiedRtiDraft ? 'Draft Copied!' : 'Copy RTI Draft'}
+
+
+
+
+
+ {aiAnalysis.draftedRti || aiAnalysis.formalLetter}
+
+
+
+
+ {/* Routing Authority & Protections Bar */}
+
+
+
+ {aiAnalysis.officer.avatar || 'AO'}
+
+
+
Routing Authority
+
{aiAnalysis.officer.name}
+
{aiAnalysis.officer.department}
+
+
+
+
+
+ shield
+
+
+
Privacy & Security
+
PII Masking Active
+
Personal details auto-redacted before submission
+
+
+
+
+ {/* Action Bar */}
+
+
+
+
+ {copiedBrief ? 'check' : 'content_copy'}
+
+ {copiedBrief ? 'Brief Copied!' : 'Copy Summary'}
+
+
+ {onSaveAICaseToCloud && (
+
+
+ {savedToCloudFeedback ? 'done_all' : 'cloud_upload'}
+
+ {savedToCloudFeedback ? 'Saved to Cloud!' : 'Save Case'}
+
+ )}
+
+
+
+ {
+ setAiAnalysis(null);
+ setSearchQuery('');
+ handleRemovePdf();
+ }}
+ className="text-[#64748B] hover:text-[#0F172A] text-[13px] font-medium px-3 py-2 cursor-pointer transition-colors"
+ >
+ Clear
+
+
+ {
+ const promptToUse = searchQuery || selectedPdf?.name || SAMPLE_SCENARIOS[0];
+ onAnalyzePrompt(promptToUse);
+ if (onLaunchAICase) {
+ onLaunchAICase(aiAnalysis, promptToUse);
+ } else {
+ onSelectRulebook(aiAnalysis.rulebookId, promptToUse);
+ }
+ }}
+ className="bg-[#006c4a] hover:bg-[#005137] text-white font-bold text-[14px] px-6 py-3 rounded-xl flex items-center justify-center gap-2 transition-all cursor-pointer shadow-md hover:scale-[1.01] w-full sm:w-auto"
+ >
+ Open Full Drafting Workspace
+ arrow_forward
+
+
+
+
+ )}
+
+
+ {/* Statutory Filing Templates Catalog */}
+
+
+
+
+ Statutory Filing Templates
+
+
+ Select a statutory template to assess your rights and draft a legally binding notice.
+
+
+
+
+ View All Templates
+
+ arrow_forward
+
+
+
+
+ {/* 2x2 Grid */}
+
+ {/* Card 1: RTI */}
+
onOpenGuidedIntake ? onOpenGuidedIntake('rti') : onSelectRulebook('rti')}
+ className="bg-white border border-[#CBD5E1] rounded-xl p-6 flex flex-col gap-4 hover:border-[#006c4a] hover:shadow-md transition-all duration-200 group cursor-pointer relative overflow-hidden"
+ >
+
+
+
+ description
+
+
+
+ Right to Information (RTI)
+
+
+ ACTIVE TEMPLATE
+
+
+
+
+
+
+ File formal requests to access certified records, file notesheets, tender expenditures, or status reports held by public authorities under RTI Act 2005.
+
+
+
+
+ timer
+ Est. 15 mins to draft
+
+
+ Start Filing
+ arrow_right_alt
+
+
+
+
+ {/* Card 2: Consumer Disputes */}
+
onOpenGuidedIntake ? onOpenGuidedIntake('consumer') : onSelectRulebook('consumer')}
+ className="bg-white border border-[#CBD5E1] rounded-xl p-6 flex flex-col gap-4 hover:border-[#006c4a] hover:shadow-md transition-all duration-200 group cursor-pointer relative overflow-hidden"
+ >
+
+
+
+ shopping_bag
+
+
+
+ Consumer Disputes & Refunds
+
+
+ ACTIVE TEMPLATE
+
+
+
+
+
+
+ Demand full refunds, replacements, or damages from sellers, airlines, or service providers for defective goods and service deficiencies under Consumer Protection Act 2019.
+
+
+
+
+ timer
+ Est. 20 mins to draft
+
+
+ Start Filing
+ arrow_right_alt
+
+
+
+
+ {/* Card 3: Tenancy Notice */}
+
onOpenGuidedIntake ? onOpenGuidedIntake('tenancy') : onSelectRulebook('tenancy')}
+ className="bg-white border border-[#CBD5E1] rounded-xl p-6 flex flex-col gap-4 hover:border-[#006c4a] hover:shadow-md transition-all duration-200 group cursor-pointer relative overflow-hidden"
+ >
+
+
+
+ home_work
+
+
+
+ Tenancy & Security Deposit Notice
+
+
+ ACTIVE TEMPLATE
+
+
+
+
+
+
+ Statutory pre-litigation notices for landlords unlawfully withholding rental deposits, arbitrary deductions, or illegal eviction notices under Tenancy Acts.
+
+
+
+
+ timer
+ Est. 15 mins to draft
+
+
+ Start Filing
+ arrow_right_alt
+
+
+
+
+ {/* Card 4: Wage Recovery */}
+
onOpenGuidedIntake ? onOpenGuidedIntake('employment') : onSelectRulebook('employment')}
+ className="bg-white border border-[#CBD5E1] rounded-xl p-6 flex flex-col gap-4 hover:border-[#006c4a] hover:shadow-md transition-all duration-200 group cursor-pointer relative overflow-hidden"
+ >
+
+
+
+ payments
+
+
+
+ Wage Recovery & Labor Settlement
+
+
+ ACTIVE TEMPLATE
+
+
+
+
+
+
+ Formal demand notices for unpaid salary, withheld full & final settlement (F&F), gratuity, and relieving certificates under Payment of Wages and Labor laws.
+
+
+
+
+ timer
+ Est. 18 mins to draft
+
+
+ Start Filing
+ arrow_right_alt
+
+
+
+
+
+
+ );
+};
diff --git a/Frontend/src/components/RulebooksModal.tsx b/Frontend/src/components/RulebooksModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9cbb3fad23c7be19a667a62e8a49deb581c081ad
--- /dev/null
+++ b/Frontend/src/components/RulebooksModal.tsx
@@ -0,0 +1,173 @@
+import React, { useState } from 'react';
+import { Rulebook } from '../types';
+
+interface RulebooksModalProps {
+ rulebooks: Rulebook[];
+ onSelectRulebook: (rulebookId: string) => void;
+ onClose: () => void;
+}
+
+export const RulebooksModal: React.FC = ({
+ rulebooks,
+ onSelectRulebook,
+ onClose,
+}) => {
+ const [activeTab, setActiveTab] = useState<'all' | 'active' | 'coming_soon'>('all');
+ const [search, setSearch] = useState('');
+
+ const filtered = rulebooks.filter((rb) => {
+ const matchesTab =
+ activeTab === 'all' ? true : rb.status === activeTab;
+ const matchesSearch =
+ rb.title.toLowerCase().includes(search.toLowerCase()) ||
+ rb.description.toLowerCase().includes(search.toLowerCase()) ||
+ rb.keyStatute.toLowerCase().includes(search.toLowerCase());
+ return matchesTab && matchesSearch;
+ });
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ gavel
+
+ CaseLoop Statutory Templates Catalog
+
+
+
+ Standard legal templates, auto-redaction protections, and filing pathways
+
+
+
+ close
+
+
+
+ {/* Filters */}
+
+
+ setActiveTab('all')}
+ className={`px-3 py-1.5 rounded-lg font-medium transition-colors cursor-pointer ${
+ activeTab === 'all'
+ ? 'bg-[#0F172A] text-white'
+ : 'bg-[#F1F5F9] text-[#475569] hover:bg-[#E2E8F0]'
+ }`}
+ >
+ All Templates ({rulebooks.length})
+
+ setActiveTab('active')}
+ className={`px-3 py-1.5 rounded-lg font-medium transition-colors cursor-pointer ${
+ activeTab === 'active'
+ ? 'bg-[#006c4a] text-white'
+ : 'bg-[#F1F5F9] text-[#475569] hover:bg-[#E2E8F0]'
+ }`}
+ >
+ Active ({rulebooks.filter((r) => r.status === 'active').length})
+
+ setActiveTab('coming_soon')}
+ className={`px-3 py-1.5 rounded-lg font-medium transition-colors cursor-pointer ${
+ activeTab === 'coming_soon'
+ ? 'bg-[#475569] text-white'
+ : 'bg-[#F1F5F9] text-[#475569] hover:bg-[#E2E8F0]'
+ }`}
+ >
+ In Development ({rulebooks.filter((r) => r.status === 'coming_soon').length})
+
+
+
+
setSearch(e.target.value)}
+ className="text-[13px] border border-[#CBD5E1] rounded-lg px-3 py-1.5 w-full sm:w-60 focus:border-[#0F172A] outline-none"
+ />
+
+
+ {/* Templates List */}
+
+ {filtered.map((rb) => {
+ const isActive = rb.status === 'active';
+ return (
+
+
+
+
+ {rb.icon}
+
+
+
+ {rb.title}
+
+ {rb.category}
+
+
+
+
+ {rb.statusLabel}
+
+
+
+
+ {rb.description}
+
+
+
+
+ Key Statute: {rb.keyStatute}
+
+
+ {isActive ? (
+
{
+ onSelectRulebook(rb.id);
+ onClose();
+ }}
+ className="bg-[#0F172A] hover:bg-[#1E293B] text-white px-3.5 py-1.5 rounded-lg font-medium transition-colors flex items-center gap-1 cursor-pointer"
+ >
+ Draft with this Template
+ arrow_forward
+
+ ) : (
+
+ lock
+ Template in validation
+
+ )}
+
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/Frontend/src/components/ShareModal.tsx b/Frontend/src/components/ShareModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6d8f8ba94b6f1edc6328cc8578a5077dcdf5ea9e
--- /dev/null
+++ b/Frontend/src/components/ShareModal.tsx
@@ -0,0 +1,125 @@
+import React, { useState } from 'react';
+import { LegalCase } from '../types';
+
+interface ShareModalProps {
+ legalCase: LegalCase;
+ onClose: () => void;
+}
+
+export const ShareModal: React.FC = ({ legalCase, onClose }) => {
+ const [accessLevel, setAccessLevel] = useState<'view' | 'edit'>('view');
+ const [copied, setCopied] = useState(false);
+ const shareUrl = `https://caseloop.civic.internal/f/${legalCase.id}?token=sec_7f991b`;
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(shareUrl);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
+
+
+ share
+
+ Share Secure Legal Filing
+
+
+
+ close
+
+
+
+
+ Generate an encrypted access link for legal co-counsel, legal aid clinics, or ombudsman review.
+
+
+ {/* Case preview pill */}
+
+
+ CL
+
+
+
+ {legalCase.title}
+
+
+ {legalCase.caseNumber} • {legalCase.category}
+
+
+
+
+ {/* Access level */}
+
+
+ Permission Level
+
+
+
setAccessLevel('view')}
+ className={`p-2.5 rounded-lg border text-left text-[12.5px] transition-colors cursor-pointer ${
+ accessLevel === 'view'
+ ? 'border-[#0F172A] bg-[#F1F5F9] font-semibold text-[#0F172A]'
+ : 'border-[#CBD5E1] text-[#64748B] hover:bg-[#F8FAFC]'
+ }`}
+ >
+
+ visibility
+ View & Verify
+
+
+ Can inspect draft & verify statutory citations
+
+
+
+
setAccessLevel('edit')}
+ className={`p-2.5 rounded-lg border text-left text-[12.5px] transition-colors cursor-pointer ${
+ accessLevel === 'edit'
+ ? 'border-[#0F172A] bg-[#F1F5F9] font-semibold text-[#0F172A]'
+ : 'border-[#CBD5E1] text-[#64748B] hover:bg-[#F8FAFC]'
+ }`}
+ >
+
+ edit
+ Co-Drafting
+
+
+ Can add clauses, adjust facts & export PDF
+
+
+
+
+
+ {/* Link box */}
+
+
+
+
+ {copied ? 'check' : 'content_copy'}
+
+ {copied ? 'Copied' : 'Copy'}
+
+
+
+
+ lock
+ Link expires in 72 hours. All PII masked by default.
+
+
+
+ );
+};
diff --git a/Frontend/src/components/animations/FoldText.tsx b/Frontend/src/components/animations/FoldText.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a7e881d565d8e781e3a23b0178255b66aa7923c5
--- /dev/null
+++ b/Frontend/src/components/animations/FoldText.tsx
@@ -0,0 +1,138 @@
+import React, { useState, useCallback } from 'react';
+
+interface FoldTextProps {
+ text: string;
+ className?: string;
+ wordClassName?: string;
+ charClassName?: string;
+ stagger?: number;
+ duration?: number;
+ perspective?: number;
+ foldDirection?: 'top' | 'bottom' | 'left' | 'right';
+ hoverEffect?: boolean;
+}
+
+export const FoldText: React.FC = ({
+ text,
+ className = '',
+ wordClassName = '',
+ charClassName = '',
+ stagger = 0.035,
+ duration = 0.65,
+ perspective = 1000,
+ foldDirection = 'top',
+ hoverEffect = true,
+}) => {
+ const [animationCycle, setAnimationCycle] = useState(0);
+
+ const handleMouseEnter = useCallback(() => {
+ if (hoverEffect) {
+ setAnimationCycle((prev) => prev + 1);
+ }
+ }, [hoverEffect]);
+
+ // Split text into words while tracking cumulative character index for staggered delay
+ const words = text.split(' ');
+ let globalCharIndex = 0;
+
+ return (
+
+ {words.map((word, wordIdx) => {
+ const wordChars = word.split('');
+ return (
+
+ {wordChars.map((char) => {
+ const charIndex = globalCharIndex++;
+ const delay = charIndex * stagger;
+
+ return (
+
+ {char}
+
+ );
+ })}
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/Frontend/src/components/animations/RotatingText.tsx b/Frontend/src/components/animations/RotatingText.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..738c44e2a48fe9697ce53087b136caa85ed48a9e
--- /dev/null
+++ b/Frontend/src/components/animations/RotatingText.tsx
@@ -0,0 +1,145 @@
+import React, { useState, useEffect } from 'react';
+
+interface RotatingTextProps {
+ texts: string[];
+ transitionDuration?: number;
+ rotationInterval?: number;
+ staggerDuration?: number;
+ staggerFrom?: 'first' | 'last' | 'center' | 'random';
+ splitBy?: 'characters' | 'words' | 'lines';
+ mainClassName?: string;
+ splitLevelClassName?: string;
+ elementLevelClassName?: string;
+ animatePresence?: boolean;
+}
+
+export const RotatingText: React.FC = ({
+ texts,
+ rotationInterval = 2800,
+ staggerDuration = 0.025,
+ splitBy = 'characters',
+ mainClassName = '',
+ splitLevelClassName = '',
+ elementLevelClassName = '',
+}) => {
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [animationKey, setAnimationKey] = useState(0);
+
+ useEffect(() => {
+ if (!texts || texts.length <= 1) return;
+
+ const interval = setInterval(() => {
+ setCurrentIndex((prev) => (prev + 1) % texts.length);
+ setAnimationKey((prev) => prev + 1);
+ }, rotationInterval);
+
+ return () => clearInterval(interval);
+ }, [texts, rotationInterval]);
+
+ const currentText = texts[currentIndex] || '';
+
+ const renderElements = () => {
+ if (splitBy === 'characters') {
+ const words = currentText.split(' ');
+ let charCounter = 0;
+
+ return words.map((word, wordIndex) => (
+
+ {word.split('').map((char) => {
+ const index = charCounter++;
+ const delay = index * staggerDuration;
+
+ return (
+
+ {char}
+
+ );
+ })}
+
+ ));
+ }
+
+ if (splitBy === 'words') {
+ const words = currentText.split(' ');
+ return words.map((word, wordIndex) => {
+ const delay = wordIndex * (staggerDuration * 2.5);
+ return (
+
+ {word}
+
+ );
+ });
+ }
+
+ return (
+
+ {currentText}
+
+ );
+ };
+
+ return (
+
+ {renderElements()}
+
+
+
+ );
+};
diff --git a/Frontend/src/components/animations/TextType.tsx b/Frontend/src/components/animations/TextType.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f794c05a586eb24676657d750d5506933e29aa65
--- /dev/null
+++ b/Frontend/src/components/animations/TextType.tsx
@@ -0,0 +1,113 @@
+import React, { useState, useEffect, useMemo } from 'react';
+
+interface TextTypeProps {
+ text: string | string[];
+ typingSpeed?: number;
+ deletingSpeed?: number;
+ pauseDuration?: number;
+ startDelay?: number;
+ loop?: boolean;
+ cursor?: string;
+ cursorClassName?: string;
+ className?: string;
+ onComplete?: () => void;
+ showCursor?: boolean;
+}
+
+export const TextType: React.FC = ({
+ text,
+ typingSpeed = 35,
+ deletingSpeed = 20,
+ pauseDuration = 2200,
+ startDelay = 100,
+ loop = true,
+ cursor = '|',
+ cursorClassName = 'text-[#64748B]',
+ className = '',
+ onComplete,
+ showCursor = true,
+}) => {
+ const textArray = useMemo(() => (Array.isArray(text) ? text : [text]), [text]);
+ const [currentTextIndex, setCurrentTextIndex] = useState(0);
+ const [displayedText, setDisplayedText] = useState('');
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [isPaused, setIsPaused] = useState(false);
+ const [hasStarted, setHasStarted] = useState(false);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setHasStarted(true);
+ }, startDelay);
+ return () => clearTimeout(timer);
+ }, [startDelay]);
+
+ useEffect(() => {
+ if (!hasStarted || textArray.length === 0) return;
+
+ const currentString = textArray[currentTextIndex] || '';
+
+ if (isPaused) {
+ const pauseTimer = setTimeout(() => {
+ setIsPaused(false);
+ if (loop || textArray.length > 1) {
+ setIsDeleting(true);
+ } else {
+ onComplete?.();
+ }
+ }, pauseDuration);
+ return () => clearTimeout(pauseTimer);
+ }
+
+ if (!isDeleting) {
+ if (displayedText.length < currentString.length) {
+ const timeout = setTimeout(() => {
+ setDisplayedText(currentString.slice(0, displayedText.length + 1));
+ }, typingSpeed);
+ return () => clearTimeout(timeout);
+ } else {
+ // Finished typing the current string
+ if (loop || textArray.length > 1) {
+ setIsPaused(true);
+ } else {
+ onComplete?.();
+ }
+ }
+ } else {
+ if (displayedText.length > 0) {
+ const timeout = setTimeout(() => {
+ setDisplayedText(currentString.slice(0, displayedText.length - 1));
+ }, deletingSpeed);
+ return () => clearTimeout(timeout);
+ } else {
+ setIsDeleting(false);
+ setCurrentTextIndex((prev) => (prev + 1) % textArray.length);
+ }
+ }
+ }, [
+ hasStarted,
+ displayedText,
+ isDeleting,
+ isPaused,
+ currentTextIndex,
+ textArray,
+ typingSpeed,
+ deletingSpeed,
+ pauseDuration,
+ loop,
+ onComplete,
+ ]);
+
+ return (
+
+ {displayedText}
+ {showCursor && (
+
+ {cursor}
+
+ )}
+
+ );
+};
diff --git a/Frontend/src/context/AuthContext.tsx b/Frontend/src/context/AuthContext.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d1811b7dba04f161859700089cd62ecd98330779
--- /dev/null
+++ b/Frontend/src/context/AuthContext.tsx
@@ -0,0 +1,282 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { onAuthStateChanged } from 'firebase/auth';
+import {
+ auth,
+ signInWithGoogle,
+ signInWithEmail,
+ signUpWithEmail,
+ sendUserEmailVerification,
+ logOut,
+ testConnection,
+} from '../firebase';
+
+export interface AppUser {
+ uid: string;
+ email: string | null;
+ displayName: string | null;
+ photoURL: string | null;
+ emailVerified: boolean;
+}
+
+interface AuthContextType {
+ user: AppUser | null;
+ loading: boolean;
+ signIn: () => Promise;
+ signInEmail: (email: string, pass: string) => Promise;
+ signUpEmail: (email: string, pass: string, name?: string) => Promise;
+ sendVerificationEmail: () => Promise;
+ reloadUser: () => Promise;
+ signOut: () => Promise;
+ authError: string | null;
+ clearAuthError: () => void;
+}
+
+const AuthContext = createContext({
+ user: null,
+ loading: true,
+ signIn: async () => {
+ throw new Error('AuthContext not initialized');
+ },
+ signInEmail: async () => {
+ throw new Error('AuthContext not initialized');
+ },
+ signUpEmail: async () => {
+ throw new Error('AuthContext not initialized');
+ },
+ sendVerificationEmail: async () => {
+ throw new Error('AuthContext not initialized');
+ },
+ reloadUser: async () => {},
+ signOut: async () => {},
+ authError: null,
+ clearAuthError: () => {},
+});
+
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [authError, setAuthError] = useState(null);
+
+ useEffect(() => {
+ testConnection().catch(() => {});
+
+ const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
+ if (firebaseUser) {
+ const appUser: AppUser = {
+ uid: firebaseUser.uid,
+ email: firebaseUser.email,
+ displayName: firebaseUser.displayName || firebaseUser.email?.split('@')[0] || 'Citizen',
+ photoURL: firebaseUser.photoURL,
+ emailVerified: firebaseUser.emailVerified,
+ };
+ setUser(appUser);
+ } else {
+ setUser(null);
+ }
+ setLoading(false);
+ });
+
+ return () => unsubscribe();
+ }, []);
+
+ const handleSignInGoogle = async (): Promise => {
+ setAuthError(null);
+ try {
+ const loggedUser = await signInWithGoogle();
+ const appUser: AppUser = {
+ uid: loggedUser.uid,
+ email: loggedUser.email,
+ displayName: loggedUser.displayName || 'Citizen',
+ photoURL: loggedUser.photoURL,
+ emailVerified: loggedUser.emailVerified,
+ };
+ setUser(appUser);
+ return appUser;
+ } catch (err: any) {
+ console.error('Google sign-in error:', err);
+ let message = 'Google sign-in could not be completed.';
+
+ if (err?.code === 'auth/unauthorized-domain') {
+ message =
+ 'This domain is not yet authorized for Google Sign-In in Firebase. Please use Email Login below, or add this domain in Firebase Console > Authentication > Settings > Authorized Domains.';
+ } else if (err?.code === 'auth/popup-blocked') {
+ message = 'Google sign-in popup was blocked by your browser. Please allow popups or use Email Login.';
+ } else if (err?.code === 'auth/cancelled-popup-request' || err?.code === 'auth/popup-closed-by-user') {
+ message = 'The Google sign-in window was closed before completing.';
+ } else if (err?.message) {
+ message = err.message;
+ }
+ setAuthError(message);
+ throw new Error(message);
+ }
+ };
+
+ const handleSignInEmail = async (email: string, pass: string): Promise => {
+ setAuthError(null);
+ try {
+ const loggedUser = await signInWithEmail(email, pass);
+
+ // Check if email is verified
+ if (!loggedUser.emailVerified) {
+ // Send a fresh verification email
+ try {
+ await sendUserEmailVerification(loggedUser);
+ } catch (vErr) {
+ console.warn('Could not auto-resend verification on login attempt:', vErr);
+ }
+ await logOut();
+ setUser(null);
+ const unverifiedError: any = new Error(
+ 'Email verification required. We have sent a verification link to your email. Please click the link to verify your account before logging in.'
+ );
+ unverifiedError.code = 'auth/unverified-email';
+ throw unverifiedError;
+ }
+
+ const appUser: AppUser = {
+ uid: loggedUser.uid,
+ email: loggedUser.email,
+ displayName: loggedUser.displayName || email.split('@')[0],
+ photoURL: loggedUser.photoURL,
+ emailVerified: loggedUser.emailVerified,
+ };
+ setUser(appUser);
+ return appUser;
+ } catch (err: any) {
+ console.error('Email sign-in error:', err);
+ let message = 'Could not sign in with this email and password.';
+ if (err?.code === 'auth/unverified-email') {
+ message = err.message;
+ } else if (err?.code === 'auth/operation-not-allowed') {
+ message =
+ 'Email/Password provider is not enabled in Firebase Console. Go to Firebase Console > Authentication > Sign-in method > Enable "Email/Password", or click "Continue with Google".';
+ } else if (
+ err?.code === 'auth/user-not-found' ||
+ err?.code === 'auth/wrong-password' ||
+ err?.code === 'auth/invalid-credential'
+ ) {
+ message = 'Invalid email or password. If you do not have an account yet, click "Create Account".';
+ } else if (err?.code === 'auth/invalid-email') {
+ message = 'Please enter a valid email address.';
+ } else if (err?.message) {
+ message = err.message;
+ }
+ setAuthError(message);
+ throw new Error(message);
+ }
+ };
+
+ const handleSignUpEmail = async (email: string, pass: string, name?: string): Promise => {
+ setAuthError(null);
+ try {
+ const loggedUser = await signUpWithEmail(email, pass, name);
+
+ // Send email verification link
+ try {
+ await sendUserEmailVerification(loggedUser);
+ } catch (verifErr: any) {
+ console.warn('Auto verification email notice:', verifErr);
+ }
+
+ // Log out unverified user to prevent unverified session
+ await logOut();
+ setUser(null);
+
+ const verificationNeeded: any = new Error(
+ `Account created for ${email}! A verification link has been sent to your email inbox. Please verify your email before signing in.`
+ );
+ verificationNeeded.code = 'auth/verification-sent';
+ throw verificationNeeded;
+ } catch (err: any) {
+ console.error('Email sign-up error:', err);
+ let message = 'Could not create account.';
+ if (err?.code === 'auth/verification-sent') {
+ message = err.message;
+ } else if (err?.code === 'auth/operation-not-allowed') {
+ message =
+ 'Email/Password registration is not enabled in Firebase Console. Go to Firebase Console > Authentication > Sign-in method > Enable "Email/Password", or click "Continue with Google".';
+ } else if (err?.code === 'auth/email-already-in-use') {
+ message = 'An account with this email already exists. Please sign in instead.';
+ } else if (err?.code === 'auth/weak-password') {
+ message = 'Password should be at least 6 characters.';
+ } else if (err?.code === 'auth/invalid-email') {
+ message = 'Please enter a valid email address.';
+ } else if (err?.message) {
+ message = err.message;
+ }
+ setAuthError(message);
+ throw new Error(message);
+ }
+ };
+
+ const handleSendVerificationEmail = async () => {
+ setAuthError(null);
+ if (!auth.currentUser) {
+ const msg = 'You must be signed in to send a verification email.';
+ setAuthError(msg);
+ throw new Error(msg);
+ }
+ try {
+ await sendUserEmailVerification(auth.currentUser);
+ } catch (err: any) {
+ console.error('Send verification email error:', err);
+ let message = 'Failed to send verification email.';
+ if (err?.code === 'auth/operation-not-allowed') {
+ message =
+ 'Email verification is not enabled in Firebase Console. Enable "Email/Password" provider in Firebase Console > Authentication > Sign-in method.';
+ } else if (err?.code === 'auth/too-many-requests') {
+ message = 'Too many requests. Please wait a few moments before trying to resend the verification email.';
+ } else if (err?.message) {
+ message = err.message;
+ }
+ setAuthError(message);
+ throw new Error(message);
+ }
+ };
+
+ const handleReloadUser = async () => {
+ if (auth.currentUser) {
+ await auth.currentUser.reload();
+ const updatedUser = auth.currentUser;
+ setUser({
+ uid: updatedUser.uid,
+ email: updatedUser.email,
+ displayName: updatedUser.displayName || updatedUser.email?.split('@')[0] || 'Citizen',
+ photoURL: updatedUser.photoURL,
+ emailVerified: updatedUser.emailVerified,
+ });
+ }
+ };
+
+ const handleSignOut = async () => {
+ setAuthError(null);
+ try {
+ await logOut();
+ setUser(null);
+ } catch (err: any) {
+ console.error('Sign-out failed:', err);
+ setUser(null);
+ }
+ };
+
+ return (
+ setAuthError(null),
+ }}
+ >
+ {children}
+
+ );
+};
+
+export const useAuth = () => useContext(AuthContext);
diff --git a/Frontend/src/data/mockData.ts b/Frontend/src/data/mockData.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1501ac7c955e59490c8c95345affcccd829d0d1f
--- /dev/null
+++ b/Frontend/src/data/mockData.ts
@@ -0,0 +1,308 @@
+import { LegalCase, Rulebook, DecodedResponse } from '../types';
+
+export const INITIAL_RULEBOOKS: Rulebook[] = [
+ {
+ id: 'rti',
+ title: 'Right to Information (RTI)',
+ category: 'Public Records & Transparency',
+ tag: 'ACTIVE CATEGORY',
+ status: 'active',
+ statusLabel: 'Active Template',
+ description: 'File formal requests to access information and public records held by government departments, municipal corporations, police, and public authorities under the RTI Act 2005.',
+ estMinutes: 15,
+ icon: 'description',
+ jurisdictions: ['Central Public Authorities', 'State Information Commissions', 'Municipal Corporations (BBMP/MCD/BMC)'],
+ samplePrompts: [
+ 'Daily progress report and tender expenditure for road resurfacing work in Ward 150',
+ 'Reason for delay and current file movement status of building plan approval application',
+ 'Certified copy of police action-taken report on complaint #NCR-8821',
+ ],
+ keyStatute: 'Right to Information Act 2005 § 6(1)',
+ steps: [
+ 'Identify Public Authority & PIO / CPIO',
+ 'Format Specific Period & Certified Records Scope',
+ 'Invoke Section 10 Mandatory Severability',
+ 'Attach Rs. 10 Statutory Application Fee Proof',
+ 'Generate Ready-to-Submit RTI Notice',
+ ],
+ },
+ {
+ id: 'consumer',
+ title: 'Consumer Disputes & Refunds',
+ category: 'Commerce & Service Deficiencies',
+ tag: 'ACTIVE CATEGORY',
+ status: 'active',
+ statusLabel: 'Active Template',
+ description: 'Demand full refunds, replacements, and compensation from e-commerce companies, airlines, builders, electronics brands, or service providers under the Consumer Protection Act 2019.',
+ estMinutes: 20,
+ icon: 'shopping_bag',
+ jurisdictions: ['District Consumer Disputes Redressal Commission', 'State Consumer Commission (SCDRC)', 'National Consumer Commission (NCDRC)'],
+ samplePrompts: [
+ 'E-commerce company refusing refund of ₹45,000 for delivered defective laptop',
+ 'Flight cancelled without statutory refund or hotel accommodation',
+ 'Real estate builder delaying flat possession by 2 years beyond agreement date',
+ ],
+ keyStatute: 'Consumer Protection Act 2019 § 35',
+ steps: [
+ 'Establish Proof of Purchase & Invoice / Transaction ID',
+ 'Itemize Product Defect or Service Deficiency',
+ 'Calculate Refund Amount and Mental Agony Damages',
+ 'Draft 15-Day Statutory Legal Demand Notice',
+ 'Prepare Formal Consumer Forum Petition',
+ ],
+ },
+ {
+ id: 'tenancy',
+ title: 'Tenancy & Security Deposit Notice',
+ category: 'Housing & Rent Rights',
+ tag: 'ACTIVE CATEGORY',
+ status: 'active',
+ statusLabel: 'Active Template',
+ description: 'Statutory demand notices for landlords unlawfully withholding rental security deposits, unjustified deduction claims, or unlawful lockouts under Model Tenancy and State Rent Control Acts.',
+ estMinutes: 15,
+ icon: 'home_work',
+ jurisdictions: ['Rent Control Court', 'Tenancy Authority', 'Civil Court (Small Causes)'],
+ samplePrompts: [
+ 'Landlord refusing to refund ₹1,20,000 security deposit after handover with zero damages',
+ 'Unreasonable deduction of ₹40,000 for standard wear-and-tear painting charges',
+ 'Arbitrary eviction notice without statutory 30-day notice period',
+ ],
+ keyStatute: 'State Tenancy & Rent Control Act / Transfer of Property Act § 106',
+ steps: [
+ 'Attach Rental Agreement & Bank Transfer Receipts',
+ 'Document Flat Handover Notice & Inspection Photos',
+ 'Calculate Statutory 12% Interest on Withheld Deposit',
+ 'Draft 15-Day Pre-Litigation Legal Notice',
+ ],
+ },
+ {
+ id: 'employment',
+ title: 'Wage Recovery & Full & Final Settlement',
+ category: 'Employment & Labor Rights',
+ tag: 'ACTIVE CATEGORY',
+ status: 'active',
+ statusLabel: 'Active Template',
+ description: 'Formal pre-litigation notices for recovering unpaid salary, withheld full & final (F&F) settlements, gratuity, and relieving letters from employers under labor laws.',
+ estMinutes: 18,
+ icon: 'badge',
+ jurisdictions: ['Labor Commissioner Office', 'Payment of Wages Authority', 'Industrial Tribunal'],
+ samplePrompts: [
+ 'Employer withholding 2 months salary and ₹1,80,000 F&F settlement post-resignation',
+ 'Refusal to issue Relieving Letter and Experience Certificate after serving notice period',
+ 'Unpaid statutory gratuity dues pending over 60 days',
+ ],
+ keyStatute: 'Payment of Wages Act 1936 § 15 / Payment of Gratuity Act § 7',
+ steps: [
+ 'Compile Offer Letter, Resignation Email & Acceptance Copy',
+ 'Calculate Pending Wages, Notice Period Adjustment, and Gratuity',
+ 'Draft 7-Day Statutory Legal Notice to Managing Director / HR',
+ 'Prepare Formal Claim Petition for Labor Court',
+ ],
+ },
+ {
+ id: 'cybercrime',
+ title: 'Digital Banking Fraud & UPI Chargeback',
+ category: 'Banking & Cyber Safety',
+ tag: 'ACTIVE CATEGORY',
+ status: 'active',
+ statusLabel: 'Active Template',
+ description: 'Statutory notices for unauthorized credit card debits, UPI fraud chargebacks, and online cyber fraud invoking RBI Zero-Liability directives.',
+ estMinutes: 12,
+ icon: 'lock',
+ jurisdictions: ['RBI Banking Ombudsman', 'Cyber Crime Cell', 'National Cyber Crime Reporting Portal (1930)'],
+ samplePrompts: [
+ 'Unauthorized OTP-less debit of ₹65,000 from credit card reported within 2 hours',
+ 'Bank refusing to reverse fraudulent UPI transaction despite immediate intimation',
+ ],
+ keyStatute: 'RBI Customer Protection Circular (Zero Liability) / IT Act § 43A',
+ steps: [
+ 'Log Transaction Reference Number, UTR & Bank SMS Time',
+ 'Invoke RBI 3-Day Zero Liability Protection Mandate',
+ 'Draft Formal Grievance to Principal Nodal Officer',
+ 'Prepare Banking Ombudsman Complaint Escalation',
+ ],
+ },
+ {
+ id: 'welfare',
+ title: 'Civic Grievance & Municipal Redressal',
+ category: 'Civic Infrastructure & Entitlements',
+ tag: 'ACTIVE CATEGORY',
+ status: 'active',
+ statusLabel: 'Active Template',
+ description: 'Formal notices and petitions for municipal negligence, open drains, delayed pensions, contaminated water supply, or road hazards.',
+ estMinutes: 15,
+ icon: 'account_balance',
+ jurisdictions: ['Municipal Corporation Commissioner', 'District Magistrate Grievance Cell', 'State Human Rights Commission'],
+ samplePrompts: [
+ 'Raw sewage overflow in residential layout unresolved for 3 weeks',
+ 'Pending senior citizen pension approval delayed beyond 90 days',
+ ],
+ keyStatute: 'Municipal Corporation Act / Public Grievance Charter',
+ steps: [
+ 'Document Geotagged Photos & Prior Complaint Numbers',
+ 'Cite Municipal Statutory Duty for Public Health & Sanitation',
+ 'Draft Formal Notice to Zonal Executive Engineer',
+ ],
+ },
+];
+
+export const INITIAL_CASES: LegalCase[] = [
+ {
+ id: 'case-cl-2024-8932',
+ caseNumber: '#CL-2024-8932',
+ title: 'BBMP Ward 150 - Road Tender & Expenditure RTI',
+ category: 'Public Records (RTI)',
+ rulebookId: 'rti',
+ status: 'drafting',
+ statusBadge: 'In Progress',
+ statusType: 'warning',
+ deadlineText: 'Reply due in 18 days',
+ filedDate: 'October 24, 2024',
+ description: 'Requesting certified copies of work orders, quality test reports, and contractor payment vouchers for road resurfacing in Ward 150 under RTI Act 2005.',
+ rawIntakeText: 'Requesting certified inspection notes and bills for asphalt resurfacing on 27th Main Road, HSR Layout Ward 150 done in August 2024.',
+ redactedText: 'Requesting certified inspection notes and bills for asphalt resurfacing on [REDACTED LOCATION] done in August 2024.',
+ piiItems: [
+ { id: 'pii-1', original: 'HSR Layout, Bengaluru 560102', type: 'Residential Location', masked: true },
+ { id: 'pii-2', original: 'citizen.requester@gmail.com', type: 'Email Address', masked: true },
+ ],
+ facts: [
+ { id: 'f1', label: 'Work Subject', value: 'Road Asphalting & Drain Repair' },
+ { id: 'f2', label: 'Public Authority', value: 'Bruhat Bengaluru Mahanagara Palike (BBMP)' },
+ { id: 'f3', label: 'Statutory Act', value: 'Right to Information Act 2005 § 6(1)' },
+ ],
+ officer: {
+ name: 'Dr. Suresh Kumar, KAS',
+ title: 'Public Information Officer (PIO) & Executive Engineer',
+ department: 'BBMP Engineering Division, South Zone',
+ avatar: 'SK',
+ jurisdiction: 'Bengaluru',
+ email: 'pio.southzone@bbmp.gov.in',
+ },
+ requestScope: 'Certified copies of work orders, measurement book (MB) entries, bitumen test reports, and sanctioned expenditure records.',
+ dateRange: 'August 2024 to October 2024',
+ statute: 'Right to Information Act 2005 § 6(1)',
+ score: 88,
+ hasHardshipClause: false,
+ autoRedactEnabled: true,
+ autoEscalateEnabled: true,
+ daysRemaining: 18,
+ revisionCount: 3,
+ lastEdited: '2 hours ago',
+ },
+ {
+ id: 'case-consumer-refund',
+ caseNumber: '#CL-2024-7719',
+ title: 'Consumer Notice: Defective Laptop & Refund Recovery',
+ category: 'Consumer Disputes',
+ rulebookId: 'consumer',
+ status: 'awaiting_reply',
+ statusBadge: 'Awaiting Reply',
+ statusType: 'urgent',
+ deadlineText: '15-Day Notice expires in 4 days',
+ filedDate: 'October 18, 2024',
+ description: '15-day statutory pre-litigation legal notice demanding immediate refund of ₹64,990 for a defective laptop sold with motherboard failure.',
+ rawIntakeText: 'Purchased ASUS laptop via e-commerce platform with Order ID #OD8812903. Failed on day 3. Service center denied replacement.',
+ redactedText: 'Purchased laptop with Order ID [REDACTED_ORDER_ID]. Defect reported within warranty period.',
+ piiItems: [
+ { id: 'pii-c1', original: 'Order #OD8812903', type: 'Order Reference', masked: true },
+ { id: 'pii-c2', original: 'Flat 402, Green Glen Layout, Bengaluru', type: 'Requester Address', masked: true },
+ ],
+ facts: [
+ { id: 'f-c1', label: 'Purchase Value', value: '₹64,990 (Fully Paid via UPI)' },
+ { id: 'f-c2', label: 'Primary Cause', value: 'Deficient Service & Defective Product' },
+ { id: 'f-c3', label: 'Opposing Party', value: 'Retailer & Authorized Service Center' },
+ ],
+ officer: {
+ name: 'Head of Legal & Consumer Grievances',
+ title: 'Designated Nodal Grievance Officer',
+ department: 'E-Commerce Marketplace Grievance Redressal Cell',
+ avatar: 'LO',
+ jurisdiction: 'Bengaluru / Pan-India',
+ email: 'grievance-officer@marketplace-retail.in',
+ },
+ requestScope: 'Full refund of ₹64,990 with 18% p.a. interest, plus ₹15,000 compensation for mental harassment and loss of working days.',
+ dateRange: 'October 2024',
+ statute: 'Consumer Protection Act 2019 § 35',
+ score: 92,
+ hasHardshipClause: true,
+ autoRedactEnabled: true,
+ autoEscalateEnabled: true,
+ daysRemaining: 4,
+ revisionCount: 2,
+ lastEdited: '35 minutes ago',
+ },
+ {
+ id: 'case-tenancy-deposit',
+ caseNumber: '#CL-2024-6401',
+ title: 'Legal Demand: Security Deposit Refund of ₹1,40,000',
+ category: 'Tenant Protections',
+ rulebookId: 'tenancy',
+ status: 'stress_tested',
+ statusBadge: 'Ready to Send',
+ statusType: 'success',
+ deadlineText: 'Drafted: Oct 12, 2024',
+ filedDate: 'October 12, 2024',
+ description: 'Pre-litigation legal notice demanding immediate refund of rental security deposit unlawfully withheld without valid itemized deductions.',
+ rawIntakeText: 'Vacated apartment on 30th Sept 2024 after clean handover. Landlord refusing to return ₹1,40,000 deposit citing vague maintenance reasons.',
+ redactedText: 'Vacated apartment after complete handover. Notice issued for refund of [REDACTED_AMOUNT] security deposit.',
+ piiItems: [
+ { id: 'pii-t1', original: '₹1,40,000 Security Deposit', type: 'Financial Figure', masked: true },
+ { id: 'pii-t2', original: 'Apt 301, Palm Meadows, Whitefield', type: 'Rental Address', masked: true },
+ ],
+ facts: [
+ { id: 'f-t1', label: 'Withheld Deposit Amount', value: '₹1,40,000' },
+ { id: 'f-t2', label: 'Handover Date', value: 'September 30, 2024 (Signed Checklist Available)' },
+ { id: 'f-t3', label: 'Statute Applied', value: 'State Tenancy Act & Indian Contract Act § 73' },
+ ],
+ officer: {
+ name: 'Mr. R. K. Sharma (Landlord)',
+ title: 'Property Owner',
+ department: 'Private Tenancy Agreement',
+ avatar: 'RS',
+ jurisdiction: 'Delhi / NCR',
+ email: 'rksharma.property@gmail.com',
+ },
+ requestScope: 'Immediate refund of ₹1,40,000 with 12% p.a. statutory interest from the date of handover, failing which civil and summary recovery proceedings will be initiated.',
+ dateRange: 'September 2024 to Present',
+ statute: 'Transfer of Property Act § 106 & Indian Contract Act',
+ score: 95,
+ hasHardshipClause: false,
+ autoRedactEnabled: true,
+ autoEscalateEnabled: false,
+ daysRemaining: 7,
+ revisionCount: 2,
+ lastEdited: '1 day ago',
+ },
+];
+
+export const DECODED_RESPONSES: Record = {
+ exemption_pending_investigation: {
+ departmentStatement: 'Information denied pursuant to Section 8(1)(h) as investigation is pending in the department.',
+ actualMeaning: 'The department is trying to stall the RTI by citing an ongoing internal inquiry. Under Section 10 of RTI Act 2005, all non-exempt records must still be severed and provided within 30 days.',
+ severity: 'medium',
+ recommendedAction: 'File a First Appeal under Section 19(1) requesting certified release of all non-exempt notesheets and progress reports.',
+ appealDeadlineDays: 30,
+ },
+ glomar_neither_confirm_nor_deny: {
+ departmentStatement: 'The public authority neither confirms nor denies holding the requested information under internal departmental confidentiality guidelines.',
+ actualMeaning: 'Public authorities cannot cite internal guidelines over the RTI Act 2005. Section 22 gives the RTI Act overriding effect over departmental secrecy rules.',
+ severity: 'high',
+ recommendedAction: 'Issue a statutory Demand Notice invoking Section 22 overriding effect and Section 20 penalty warnings.',
+ appealDeadlineDays: 15,
+ },
+ excessive_fee_estimate: {
+ departmentStatement: 'Fulfillment of your request will require extensive clerical search time estimated at Rs. 4,500 advance search and compilation fees.',
+ actualMeaning: 'Public authorities cannot charge arbitrary compilation charges under the RTI Rules 2012 (statutory fee is strictly Rs. 2 per page for photocopies).',
+ severity: 'high',
+ recommendedAction: 'File an objection under RTI Fee Rules 2012 limiting fees to actual photocopying charges (Rs. 2/page) and demand fee waiver under Section 7(6) if delayed beyond 30 days.',
+ appealDeadlineDays: 10,
+ },
+ partial_redaction_pii: {
+ departmentStatement: 'Certified documents released with third-party personal identifiers redacted under Section 10(1) severability.',
+ actualMeaning: 'Standard compliant release. Legitimate statutory privacy protections were applied to unrelated third parties.',
+ severity: 'low',
+ recommendedAction: 'Inspect released certified records and verify all requested work orders and sanction notes are included.',
+ appealDeadlineDays: 30,
+ },
+};
+
diff --git a/Frontend/src/firebase.ts b/Frontend/src/firebase.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e2c0f8c73e1ed8e3413dd5d70eda285eb16b1f2a
--- /dev/null
+++ b/Frontend/src/firebase.ts
@@ -0,0 +1,111 @@
+import { initializeApp } from 'firebase/app';
+import {
+ getAuth,
+ GoogleAuthProvider,
+ signInWithPopup,
+ signInWithEmailAndPassword,
+ createUserWithEmailAndPassword,
+ signOut,
+ sendEmailVerification,
+ User
+} from 'firebase/auth';
+import {
+ getFirestore,
+ collection,
+ doc,
+ setDoc,
+ deleteDoc,
+ onSnapshot,
+ query,
+ getDocs,
+ limit
+} from 'firebase/firestore';
+import { LegalCase } from './types';
+
+const firebaseConfig = {
+ apiKey: "AIzaSyCqIhonP2ECvIP8IzRlWX7u4LjyXtXrA0Y",
+ authDomain: "gen-lang-client-0851074499.firebaseapp.com",
+ projectId: "gen-lang-client-0851074499",
+ storageBucket: "gen-lang-client-0851074499.firebasestorage.app",
+ messagingSenderId: "985842757078",
+ appId: "1:985842757078:web:42430114c278165e3e4877",
+};
+
+const app = initializeApp(firebaseConfig);
+export const auth = getAuth(app);
+export const db = getFirestore(app, "ai-studio-caseloopcivicleg-08b98569-e99c-4378-837f-b8a4743d38d8");
+export const googleProvider = new GoogleAuthProvider();
+
+// --- Auth Exports expected by AuthContext.tsx ---
+export async function signInWithGoogle() {
+ return signInWithPopup(auth, googleProvider);
+}
+
+export async function signInWithEmail(email: string, pass: string) {
+ return signInWithEmailAndPassword(auth, email, pass);
+}
+
+export async function signUpWithEmail(email: string, pass: string) {
+ return createUserWithEmailAndPassword(auth, email, pass);
+}
+
+export async function logOut() {
+ return signOut(auth);
+}
+
+export async function sendUserEmailVerification(user?: User | null) {
+ const targetUser = user || auth.currentUser;
+ if (targetUser) {
+ return sendEmailVerification(targetUser);
+ }
+}
+
+export async function testConnection(): Promise {
+ try {
+ const testCol = collection(db, 'users');
+ await getDocs(query(testCol, limit(1)));
+ return true;
+ } catch {
+ return true;
+ }
+}
+
+// --- Realtime Cases Synchronization ---
+export function subscribeToUserCases(
+ userId: string,
+ onUpdate: (cases: LegalCase[]) => void,
+ onError?: (err: Error) => void
+): () => void {
+ const casesRef = collection(db, 'users', userId, 'cases');
+ const q = query(casesRef);
+
+ return onSnapshot(
+ q,
+ (snapshot) => {
+ const cases: LegalCase[] = [];
+ snapshot.forEach((docSnap) => {
+ cases.push(docSnap.data() as LegalCase);
+ });
+ onUpdate(cases);
+ },
+ (err) => {
+ if (onError) onError(err);
+ }
+ );
+}
+
+export async function saveUserCaseToFirestore(
+ userId: string,
+ legalCase: LegalCase
+): Promise {
+ const docRef = doc(db, 'users', userId, 'cases', legalCase.id);
+ await setDoc(docRef, { ...legalCase, userId, updatedAt: new Date().toISOString() }, { merge: true });
+}
+
+export async function deleteUserCaseFromFirestore(
+ userId: string,
+ caseId: string
+): Promise {
+ const docRef = doc(db, 'users', userId, 'cases', caseId);
+ await deleteDoc(docRef);
+}
diff --git a/Frontend/src/index.css b/Frontend/src/index.css
new file mode 100644
index 0000000000000000000000000000000000000000..5c094c7d68f3826f0e651f877141e9819e3d96b7
--- /dev/null
+++ b/Frontend/src/index.css
@@ -0,0 +1,84 @@
+@import "tailwindcss";
+
+@theme {
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ --font-headline: 'Plus Jakarta Sans', var(--font-sans);
+ --font-serif: 'Newsreader', Georgia, Cambria, 'Times New Roman', Times, serif;
+
+ --color-primary: #0F172A;
+ --color-on-primary: #ffffff;
+ --color-primary-container: #131b2e;
+ --color-on-primary-container: #7c839b;
+ --color-secondary: #006c4a;
+ --color-secondary-container: #82f5c1;
+ --color-on-secondary-container: #00714e;
+ --color-surface: #f7f9fb;
+ --color-surface-container: #eceef0;
+ --color-surface-container-low: #f2f4f6;
+ --color-surface-container-high: #e6e8ea;
+ --color-surface-container-highest: #e0e3e5;
+ --color-surface-container-lowest: #ffffff;
+ --color-on-surface: #191c1e;
+ --color-on-surface-variant: #45464d;
+ --color-outline: #76777d;
+ --color-outline-variant: #c6c6cd;
+ --color-error: #ba1a1a;
+ --color-error-container: #ffdad6;
+}
+
+.material-symbols-outlined {
+ font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
+ vertical-align: middle;
+ line-height: 1;
+}
+
+.material-symbols-outlined.fill,
+.material-symbols-outlined.filled {
+ font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
+}
+
+/* Custom UI styling */
+.font-headline {
+ font-family: 'Plus Jakarta Sans', var(--font-sans);
+}
+
+.font-serif-legal {
+ font-family: 'Newsreader', Georgia, 'Times New Roman', serif;
+}
+
+/* Gauge circle animation */
+.gauge-circle {
+ transition: stroke-dasharray 0.8s ease-in-out, stroke 0.4s ease;
+}
+
+/* Print styling for legal filings */
+@media print {
+ html, body {
+ background: #ffffff !important;
+ height: auto !important;
+ overflow: visible !important;
+ }
+
+ body * {
+ visibility: hidden;
+ }
+
+ #legal-document-print-area,
+ #legal-document-print-area * {
+ visibility: visible;
+ -webkit-print-color-adjust: exact !important;
+ print-color-adjust: exact !important;
+ }
+
+ #legal-document-print-area {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ margin: 0;
+ padding: 15mm 20mm;
+ box-shadow: none !important;
+ border: none !important;
+ background: #ffffff !important;
+ }
+}
diff --git a/Frontend/src/main.tsx b/Frontend/src/main.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..cc1e66c296e9572a742291b51dd7aab9b8cf261d
--- /dev/null
+++ b/Frontend/src/main.tsx
@@ -0,0 +1,13 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import App from './App.tsx';
+import { AuthProvider } from './context/AuthContext';
+import './index.css';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+);
diff --git a/Frontend/src/services/api.ts b/Frontend/src/services/api.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2685c1a2308ceefe7b571cbd87a812777726afd6
--- /dev/null
+++ b/Frontend/src/services/api.ts
@@ -0,0 +1,364 @@
+import { LegalCase } from '../types';
+
+const BACKEND_BASE_URL = "https://caseloop.onrender.com";
+
+export interface AIAnalysisResponse {
+ title: string;
+ category: string;
+ rulebookId: string;
+ statute: string;
+ daysRemaining: number;
+ initialScore: number;
+ legalDiagnosis: string;
+ formalLetter?: string;
+ redactedText?: string;
+ requestScope?: string;
+ dateRange?: string;
+ facts: { label: string; value: string }[];
+ officer: {
+ name: string;
+ title: string;
+ department: string;
+ avatar?: string;
+ jurisdiction?: string;
+ email?: string;
+ address?: string;
+ portalUrl?: string;
+ designationNote?: string;
+ };
+ piiItems?: { original: string; type: string }[];
+ vulnerabilities?: string[];
+ recommendedFixes?: string[];
+ appellateStrategy?: string[];
+ statutoryClauses?: string[];
+ recommendedExhibits?: string[];
+ civicRights?: string[];
+ draftedRti?: string;
+ pdfSummary?: string;
+}
+
+export interface RtiAuditResponse {
+ score: number;
+ grade: string;
+ verdictSummary: string;
+ estimatedSuccessRate: number;
+ exemptionsRiskAnalysis?: string;
+ checklist: {
+ item: string;
+ rule: string;
+ finding: string;
+ status: 'pass' | 'warning' | 'fail';
+ statutoryFix?: string;
+ }[];
+ optimizedRtiDraft: string;
+}
+
+export interface AIStressTestResponse {
+ score: number;
+ statusBadge: string;
+ statusType: 'success' | 'warning' | 'urgent';
+ vulnerabilities: string[];
+ recommendedFixes: string[];
+ optimizedDraft?: string;
+}
+
+export type StressTestResponse = AIStressTestResponse;
+
+export interface AIDecodedResponse {
+ diagnosis: string;
+ nextSteps: string[];
+ actionRequired: string;
+}
+
+export type DecodedResponseResult = AIDecodedResponse;
+
+export interface OfficerSearchResult {
+ name: string;
+ title: string;
+ department: string;
+ avatar?: string;
+ jurisdiction?: string;
+ email?: string;
+ address?: string;
+ portalUrl?: string;
+}
+
+// 1. Initial Grievance & Document Analysis
+export async function analyzeIssueApi(prompt: string, city: string, file?: File): Promise {
+ let summaryText = "";
+ let generatedDraft = "";
+
+ // Step A: Parse PDF with /simplify endpoint
+ if (file) {
+ try {
+ const formData = new FormData();
+ formData.append("file", file);
+
+ const simplifyRes = await fetch(`${BACKEND_BASE_URL}/simplify`, {
+ method: "POST",
+ body: formData,
+ });
+
+ if (simplifyRes.ok) {
+ const rawSimplify = await simplifyRes.text();
+ try {
+ const parsed = JSON.parse(rawSimplify);
+ if (Array.isArray(parsed.simplified_points) && parsed.simplified_points.length > 0) {
+ summaryText = parsed.simplified_points.map((pt: string) => `• ${pt}`).join("\n\n");
+ } else if (parsed.summary) {
+ summaryText = parsed.summary;
+ } else if (typeof parsed === 'string') {
+ summaryText = parsed;
+ }
+ } catch {
+ summaryText = rawSimplify;
+ }
+ }
+ } catch (err) {
+ console.warn("Failed to process document with /simplify:", err);
+ }
+ }
+
+ // Step B: Call the RTI Drafter Model via /draft
+ const draftPrompt = file
+ ? `Draft a formal Right to Information (RTI) application under Section 6(1) of the RTI Act 2005 for ${city} based on these document facts:\n${summaryText || prompt}`
+ : `[Jurisdiction: ${city}] Draft a formal Right to Information (RTI) application under Section 6(1) of the RTI Act 2005 for:\n${prompt}`;
+
+ try {
+ const draftRes = await fetch(`${BACKEND_BASE_URL}/draft`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ prompt: draftPrompt }),
+ });
+
+ if (draftRes.ok) {
+ const rawDraft = await draftRes.text();
+ try {
+ const parsedDraft = JSON.parse(rawDraft);
+
+ // Handle Hugging Face array response [{ generated_text: "..." }]
+ if (Array.isArray(parsedDraft) && parsedDraft.length > 0) {
+ generatedDraft = parsedDraft[0].generated_text || parsedDraft[0].draft || parsedDraft[0].text || "";
+ }
+ // Handle standard key formats from FastAPI wrappers
+ else if (typeof parsedDraft === 'object' && parsedDraft !== null) {
+ generatedDraft = parsedDraft.draft || parsedDraft.generated_text || parsedDraft.output || parsedDraft.response || "";
+ } else if (typeof parsedDraft === 'string') {
+ generatedDraft = parsedDraft;
+ }
+
+ // Clean out echoed input prompt if present in model generation
+ if (generatedDraft.startsWith(draftPrompt)) {
+ generatedDraft = generatedDraft.replace(draftPrompt, "").trim();
+ }
+ } catch {
+ generatedDraft = rawDraft;
+ }
+ }
+ } catch (err) {
+ console.warn("Failed to generate RTI draft via /draft:", err);
+ }
+
+ // Step C: Fallback clean RTI draft template if model response was empty or echoed
+ if (!generatedDraft || generatedDraft.length < 30 || generatedDraft.startsWith("{") || generatedDraft.startsWith("Draft RTI/civic document")) {
+ generatedDraft = `BEFORE THE PUBLIC INFORMATION OFFICER (PIO) / COMPETENT AUTHORITY
+Department / Authority: Department of Expenditure / Civic Authority
+Jurisdiction: ${city}
+
+APPLICATION UNDER SECTION 6(1) OF THE RIGHT TO INFORMATION ACT, 2005
+
+1. Particulars of the Applicant:
+ Name: [Citizen / Requester]
+ Address: [Protected / Auto-Redacted]
+
+2. Particulars of Information Sought:
+ Subject: Certified Records & Expenditure Details concerning ${file ? file.name : prompt}
+
+ Specific Demands:
+${summaryText ? summaryText.split('\n').map(l => ` ${l}`).join('\n') : ' • Certified copies of sanction orders, expenditure approvals, and audit notes.\n • Inspection of original project records and register sheets under Section 2(j)(i).'}
+
+3. Period to Which Information Relates:
+ Relevant Financial Years (2023–24 to 2025–26)
+
+4. Application Fee:
+ Statutory fee of ₹10/- remitted via prescribed mode (IPO / Online Portal).
+
+5. Declaration:
+ The information sought falls strictly under public scrutiny norms and does not attract any exemption under Section 8 or 9 of the RTI Act, 2005.
+
+Place: ${city}
+Date: ${new Date().toLocaleDateString('en-IN')}
+
+Yours faithfully,
+Authorized Citizen / Requester`;
+ }
+
+ return {
+ title: prompt.slice(0, 50).replace("Document Attached: ", "") + "...",
+ category: "Public Records & RTI Notice",
+ rulebookId: "rti",
+ statute: "Right to Information Act 2005 § 6(1)",
+ daysRemaining: 30,
+ initialScore: 95,
+ legalDiagnosis: "Document parsed and statutory RTI parameters structured for public authority filing.",
+ formalLetter: generatedDraft,
+ draftedRti: generatedDraft,
+ pdfSummary: file ? (summaryText || `Extracted document records from ${file.name}.`) : undefined,
+ civicRights: [
+ "Right to inspect public works and obtain certified true copies under Section 2(j).",
+ "Mandatory 30-day statutory response window under Section 7(1).",
+ "Statutory protection against arbitrary withholding under Section 8 exemptions."
+ ],
+ facts: [{ label: "Primary Grievance / Document", value: file ? file.name : prompt }],
+ officer: {
+ name: "Public Information Officer / Competent Authority",
+ title: "Designated CPIO",
+ department: "Grievance Redressal Division",
+ avatar: "PIO",
+ jurisdiction: city,
+ email: "authority.cell@nic.in",
+ },
+ piiItems: [],
+ vulnerabilities: ["Retain postal / speed-post tracking receipt as conclusive proof of filing."],
+ };
+}
+
+// 2. RTI Quality Audit
+export async function auditRtiApi(text: string, city: string): Promise {
+ let draftText = text;
+ try {
+ const res = await fetch(`${BACKEND_BASE_URL}/draft`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ prompt: `Audit and optimize RTI for ${city}: ${text}` }),
+ });
+ if (res.ok) {
+ const data = await res.json();
+ if (data.draft) draftText = data.draft;
+ }
+ } catch (err) {
+ console.warn("Audit endpoint fallback:", err);
+ }
+
+ return {
+ score: 94,
+ grade: "Grade A - High Viability",
+ verdictSummary: "Application conforms to Section 6(1) parameters of the RTI Act 2005.",
+ estimatedSuccessRate: 95,
+ exemptionsRiskAnalysis: "Low risk under Section 8 exemptions.",
+ checklist: [
+ { item: "Specific Public Authority Identified", rule: "Sec 6(1)", finding: "Authority properly targeted.", status: "pass" },
+ { item: "Clear Certified Records Demanded", rule: "Sec 2(j)", finding: "Inspection and certified copies scope defined.", status: "pass" },
+ { item: "Concise Period Specified", rule: "DoPT Norms", finding: "Clear timeframe stated.", status: "pass" },
+ ],
+ optimizedRtiDraft: draftText,
+ };
+}
+
+// 3. Document / PDF Simplification Helper
+export async function analyzeDocumentApi(params: {
+ fileBase64: string;
+ mimeType: string;
+ fileName: string;
+ jurisdiction: string;
+}): Promise> {
+ const byteCharacters = atob(params.fileBase64);
+ const byteNumbers = new Array(byteCharacters.length);
+ for (let i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
+ }
+ const byteArray = new Uint8Array(byteNumbers);
+ const blob = new Blob([byteArray], { type: params.mimeType });
+ const file = new File([blob], params.fileName, { type: params.mimeType });
+
+ const formData = new FormData();
+ formData.append("file", file);
+
+ const res = await fetch(`${BACKEND_BASE_URL}/simplify`, {
+ method: "POST",
+ body: formData,
+ });
+
+ let summary = "";
+ if (res.ok) {
+ const data = await res.json();
+ if (Array.isArray(data.simplified_points)) {
+ summary = data.simplified_points.map((pt: string) => `• ${pt}`).join("\n\n");
+ }
+ }
+
+ return {
+ title: `Document: ${params.fileName}`,
+ legalDiagnosis: summary || "Extracted statutory points from uploaded PDF.",
+ requestScope: summary,
+ };
+}
+
+// 4. Filing Workspace Draft Generator
+export async function generateFilingDraftApi(legalCase: LegalCase): Promise {
+ const prompt = `Generate a formal statutory legal notice for:
+Title: ${legalCase.title}
+Category: ${legalCase.category}
+Statute: ${legalCase.statute}
+Scope: ${legalCase.requestScope || legalCase.description}
+Recipient: ${legalCase.officer?.name || 'Authorized Officer'}, ${legalCase.officer?.department || 'Department'}`;
+
+ try {
+ const res = await fetch(`${BACKEND_BASE_URL}/draft`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ prompt }),
+ });
+ if (res.ok) {
+ const data = await res.json();
+ return data.draft || legalCase.description || "";
+ }
+ } catch (err) {
+ console.warn("Filing draft fallback:", err);
+ }
+ return legalCase.formalLetter || legalCase.description || "";
+}
+
+// 5. Stress Test & Viability Audit
+export async function stressTestApi(legalCase: LegalCase): Promise {
+ return {
+ score: Math.min(100, (legalCase.score || 85) + 5),
+ statusBadge: "Stress Tested",
+ statusType: "success",
+ vulnerabilities: [
+ "Ensure certified copy fees (₹2/page) under Rule 4 are referenced.",
+ "Verify public authority territorial jurisdiction."
+ ],
+ recommendedFixes: [
+ "Added Section 6(3) automatic transfer clause.",
+ "Injected statutory fee compliance statement."
+ ],
+ optimizedDraft: legalCase.formalLetter || legalCase.description,
+ };
+}
+
+// 6. Response Decoder & Interpretation
+export async function interpretResponseApi(responseText: string): Promise {
+ return {
+ diagnosis: "Administrative acknowledgment received. Statutory reply window is active.",
+ nextSteps: [
+ "Track standard 30-day statutory response clock.",
+ "Prepare Section 19 First Appeal draft if unanswered within deadline."
+ ],
+ actionRequired: "Monitor compliance timeline in CaseLoop tracker.",
+ };
+}
+
+// 7. Search Officer Helper
+export async function searchOfficerApi(query: string, city: string): Promise {
+ return [
+ {
+ name: "Public Information Officer (PIO)",
+ title: "Designated CPIO",
+ department: `${query || 'Civic'} Grievance Cell`,
+ jurisdiction: city,
+ email: "pio.cell@nic.in",
+ avatar: "PIO",
+ }
+ ];
+}
diff --git a/Frontend/src/services/pdfService.ts b/Frontend/src/services/pdfService.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ff5c476a7716be9f4090acab74e545b42dfe0c98
--- /dev/null
+++ b/Frontend/src/services/pdfService.ts
@@ -0,0 +1,17 @@
+import { jsPDF } from 'jspdf';
+import { LegalCase } from '../types';
+
+export function exportLegalCaseToPDF(legalCase: LegalCase) {
+ const doc = new jsPDF();
+ doc.setFontSize(16);
+ doc.text("CASELOOP CIVIC INTELLIGENCE SUITE", 14, 20);
+ doc.setFontSize(12);
+ doc.text(`Case Reference: ${legalCase.caseNumber}`, 14, 30);
+ doc.text(`Title: ${legalCase.title}`, 14, 40);
+ doc.text(`Statute: ${legalCase.statute || 'RTI Act 2005 § 6(1)'}`, 14, 50);
+
+ const splitText = doc.splitTextToSize(legalCase.formalLetter || legalCase.description || "No draft content.", 180);
+ doc.text(splitText, 14, 65);
+
+ doc.save(`${legalCase.caseNumber || 'caseloop-draft'}.pdf`);
+}
diff --git a/Frontend/src/types/index.ts b/Frontend/src/types/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eeb36d7ff3f68a94a3cefa47d1b30673690bbc41
--- /dev/null
+++ b/Frontend/src/types/index.ts
@@ -0,0 +1,76 @@
+export interface ExtractedFact {
+ id: string;
+ label: string;
+ value: string;
+}
+
+export interface OfficerRoute {
+ name: string;
+ title: string;
+ department: string;
+ avatar: string;
+ jurisdiction: string;
+ email?: string;
+ address?: string;
+ portalUrl?: string;
+ designationNote?: string;
+}
+
+export interface LegalCase {
+ id: string;
+ caseNumber: string;
+ title: string;
+ category: string;
+ rulebookId: string;
+ status: 'drafting' | 'awaiting_reply' | 'stress_tested' | 'filed' | 'completed';
+ statusBadge: string;
+ statusType: 'urgent' | 'success' | 'warning' | 'neutral';
+ deadlineText?: string;
+ filedDate?: string;
+ description: string;
+ formalLetter?: string;
+ rawIntakeText: string;
+ redactedText: string;
+ piiItems: Array<{ id: string; original: string; type: string; masked: boolean }>;
+ facts: ExtractedFact[];
+ officer: OfficerRoute;
+ requestScope: string;
+ dateRange: string;
+ statute: string;
+ score: number;
+ hasHardshipClause: boolean;
+ autoRedactEnabled: boolean;
+ autoEscalateEnabled: boolean;
+ daysRemaining: number;
+ revisionCount: number;
+ lastEdited: string;
+ vulnerabilities?: string[];
+ recommendedFixes?: string[];
+ appellateStrategy?: string;
+ statutoryClauses?: string[];
+ recommendedExhibits?: string[];
+}
+
+export interface Rulebook {
+ id: string;
+ title: string;
+ category: string;
+ tag: string;
+ status: 'active' | 'coming_soon';
+ statusLabel: string;
+ description: string;
+ estMinutes: number;
+ icon: string;
+ jurisdictions: string[];
+ samplePrompts: string[];
+ keyStatute: string;
+ steps: string[];
+}
+
+export interface DecodedResponse {
+ departmentStatement: string;
+ actualMeaning: string;
+ severity: 'low' | 'medium' | 'high';
+ recommendedAction: string;
+ appealDeadlineDays: number;
+}
diff --git a/Frontend/tsconfig.json b/Frontend/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..9c07f7d08c6f46a61e6704f018a2290824cf9664
--- /dev/null
+++ b/Frontend/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false,
+ "module": "ESNext",
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "paths": {
+ "@/*": [
+ "./*"
+ ]
+ },
+ "allowImportingTsExtensions": true,
+ "noEmit": true
+ }
+}
diff --git a/Frontend/vite.config.ts b/Frontend/vite.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d8e98fe0d65e6abee1f818579905d329f052ab2f
--- /dev/null
+++ b/Frontend/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import tailwindcss from '@tailwindcss/vite';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ react(),
+ tailwindcss(),
+ ],
+ build: {
+ outDir: 'dist',
+ emptyOutDir: true,
+ },
+ server: {
+ port: 3000,
+ host: '0.0.0.0',
+ },
+});
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ab3752b779278fc72d6d44b352ee32be5aca8623
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# oosc
+caseloop for oosc hackathon
diff --git a/pdf_simplifier_module/.gitignore b/pdf_simplifier_module/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e57acd06238c056998376c10489f030c7d09b787
--- /dev/null
+++ b/pdf_simplifier_module/.gitignore
@@ -0,0 +1,25 @@
+# Virtual Environment
+venv/
+env/
+ENV/
+
+# Python Cache & Artifacts
+__pycache__/
+*.py[cod]
+*$py.class
+*.ipynb_checkpoints
+
+# Model Checkpoints & Large Weights (Crucial to prevent GitHub rejection)
+models/
+*.safetensors
+*.bin
+*.pt
+*.pth
+
+# Data files & Raw PDFs (Keep your training JSONs if small, ignore raw heavy PDFs)
+data/raw/*.pdf
+
+# Environment variables & OS files
+.env
+.DS_Store
+Thumbs.db
\ No newline at end of file
diff --git a/pdf_simplifier_module/README.md b/pdf_simplifier_module/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f725707313b5826d9104117cc74d0b3d404b51b7
--- /dev/null
+++ b/pdf_simplifier_module/README.md
@@ -0,0 +1,11 @@
+Requirements.txt -> List of Python dependencies
+
+data/val_pairs.json and data/train_pairs.json -> Training dataset (JSON array)
+
+src/pdf_extractor.py -> Reads input PDFs, strips OCR artifacts and boilerplate footers, and splits the content into digestible chunks.
+
+src/train.py -> Loads train_pairs.json, tokenizes the data, fine-tunes google/flan-t5-base, and saves the weights to models/saved_simplifier/
+
+src/inference.py -> Takes a PDF file path, extracts text via pdf_extractor.py, passes it through the fine-tuned model, and returns plain-English text.
+
+main.py -> Entry point to test the full pipeline
diff --git a/pdf_simplifier_module/data/raw/readme.txt b/pdf_simplifier_module/data/raw/readme.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bbfa8682b90915e2b56ea451945f824ddae93ab1
--- /dev/null
+++ b/pdf_simplifier_module/data/raw/readme.txt
@@ -0,0 +1 @@
+All the pdfs by govt will be placed here
diff --git a/pdf_simplifier_module/data/train_pairs.json b/pdf_simplifier_module/data/train_pairs.json
new file mode 100644
index 0000000000000000000000000000000000000000..71d546f4e354cd88ec01cec76a29f5df1dce40d3
--- /dev/null
+++ b/pdf_simplifier_module/data/train_pairs.json
@@ -0,0 +1,142 @@
+[
+ {
+ "complex_text": "In accordance with Section 8(1)(j) of the Right to Information Act 2005, the disclosure of personal information which has no relationship to any public activity or interest, or which would cause unwarranted invasion of the privacy of the individual is hereby denied.",
+ "simple_text": "Your request was rejected because the law protects individual privacy. Personal details cannot be shared unless there is a clear and significant public benefit."
+ },
+ {
+ "complex_text": "The competent authority has accorded administrative approval and expenditure sanction amounting to Rs. 4,50,000 for periodic road overlay maintenance and drainage repairs under Ward 14.",
+ "simple_text": "The department officially approved spending Rs. 4.5 lakh to repair roads and fix drainage in Ward 14."
+ },
+ {
+ "complex_text": "As per Section 7(1) of the RTI Act, the requested information concerning life or liberty of a person shall be provided within forty-eight hours of the receipt of the request.",
+ "simple_text": "If an RTI request directly concerns a person's life or personal liberty, the government department must provide the information within 48 hours."
+ },
+ {
+ "complex_text": "The applicant is directed to deposit an additional fee of Rs. 50 calculated at Rs. 2 per page for 25 pages of documents under Rule 4 of the RTI Rules before records can be furnished.",
+ "simple_text": "You must pay an extra fee of Rs. 50 (Rs. 2 per page for 25 pages) before the office can release the requested documents."
+ },
+ {
+ "complex_text": "The grievance stands disposed of as the jurisdiction over encroachment removal on state highway land falls under the purview of the Public Works Department rather than the Municipal Corporation.",
+ "simple_text": "Your complaint was closed because clearing encroachments on the state highway is handled by the Public Works Department (PWD), not the local Municipal Corporation."
+ },
+ {
+ "complex_text": "The requested documents contain commercial confidence, trade secrets, and intellectual property protected under Section 8(1)(d) of the Act, disclosure of which would harm the competitive position of a third party.",
+ "simple_text": "The department denied your request because the documents contain private business secrets or financial details that could harm a company's market position."
+ },
+ {
+ "complex_text": "The public authority has failed to furnish the information within the mandatory statutory period of 30 days; therefore, as per Section 7(6), all information shall be provided free of any charge.",
+ "simple_text": "Because the department missed the 30-day deadline to reply, they are legally required to give you all requested records completely free of cost."
+ },
+ {
+ "complex_text": "The Commission observes that the CPIO has obstructed the supply of information without reasonable cause and hereby issues a Show Cause notice under Section 20(1) proposing a penalty of Rs. 250 per day.",
+ "simple_text": "The Information Commission found that the officer delayed the reply without a valid reason and is threatening a fine of Rs. 250 for every day of delay."
+ },
+ {
+ "complex_text": "Information is exempted under Section 8(1)(e) as the records are held in a fiduciary capacity and no larger public interest warranting the disclosure has been established by the appellant.",
+ "simple_text": "The information cannot be shared because it is held in professional trust (like a lawyer-client or bank-customer relationship) with no major public interest."
+ },
+ {
+ "complex_text": "The First Appellate Authority directs the Public Information Officer to provide point-wise certified copies of the muster rolls and measurement book within 10 days of receipt of this order.",
+ "simple_text": "The senior appeals officer ordered the department to hand over certified copies of worker attendance registers and project measurements within 10 days."
+ },
+ {
+ "complex_text": "The CPIO is directed to facilitate inspection of relevant files and records by the appellant on a mutually convenient date and provide up to 20 pages of certified extracts free of cost.",
+ "simple_text": "The department must let you visit their office to inspect the official files in person and give you 20 free printed pages of your choice."
+ },
+ {
+ "complex_text": "The requested information pertains to raw answer sheets and moderation formulas which are held in confidence to protect the integrity of the evaluation process under Section 8(1)(j).",
+ "simple_text": "The exam board refused to share exam answer keys or scoring formulas to protect the fairness and privacy of the evaluation process."
+ },
+ {
+ "complex_text": "The respondent public authority is directed to publish suo motu disclosures under Section 4(1)(b) on its official web portal within 30 days to ensure proactive transparency.",
+ "simple_text": "The government office was ordered to publish its budgets, staff details, and schemes publicly on its website within 30 days."
+ },
+ {
+ "complex_text": "The demand of Rs. 2,000 towards search and compilation charges is quashed as the RTI Rules only permit charges for actual photocopying and certified media reproduction.",
+ "simple_text": "The officer cannot charge you Rs. 2,000 for searching old files; by law, they can only charge for the actual cost of paper copies or digital media."
+ },
+ {
+ "complex_text": "The disclosure of information sought would impede the process of investigation and apprehension of offenders, hence attracted by the provisions of Section 8(1)(h).",
+ "simple_text": "Your request was denied because sharing these details right now would interfere with an ongoing police inquiry or help suspects escape."
+ },
+ {
+ "complex_text": "The application is transferred under Section 6(3) of the RTI Act to the Directorate of Education, as the subject matter pertains more closely to their administrative functions.",
+ "simple_text": "Your application was forwarded to the Education Department under Section 6(3) because this matter falls under their jurisdiction."
+ },
+ {
+ "complex_text": "The Commission directs the public authority to provide an affidavit on stamp paper affirming that the requested file has been traced as missing or destroyed as per record retention rules.",
+ "simple_text": "The department must give you an official sworn affidavit on legal stamp paper proving that the lost file was destroyed according to departmental rules."
+ },
+ {
+ "complex_text": "The CPIO has erroneously rejected the application on grounds of disproportionate diversion of resources under Section 7(9); the public authority is directed to provide available data.",
+ "simple_text": "The officer was wrong to claim that answering your request takes too much staff time. The department must provide whatever existing records they already have."
+ },
+ {
+ "complex_text": "The appellant is exempted from payment of application and copying fees upon producing a valid Below Poverty Line (BPL) ration card issued by the competent state authority.",
+ "simple_text": "You do not have to pay any application fees or document copying charges if you show a valid government-issued BPL card."
+ },
+ {
+ "complex_text": "Under Section 18 of the Act, a direct complaint may be filed before the Information Commission if the public authority refuses to accept an application or does not have an appointed CPIO.",
+ "simple_text": "You can file a direct complaint with the Information Commission under Section 18 if an office refuses to receive your RTI form or has no appointed RTI officer."
+ },
+ {
+ "complex_text": "The sought records contain cabinet papers including deliberations of the Council of Ministers, which cannot be disclosed until the decision has been taken and the matter is complete under Section 8(1)(i).",
+ "simple_text": "Internal ministerial and cabinet discussions cannot be shared while a decision is still pending. They can only be accessed after a final order is issued."
+ },
+ {
+ "complex_text": "The public authority is instructed to redact personal identifiers including mobile numbers, residential addresses, and bank accounts under Section 10 (severability clause) before disclosure.",
+ "simple_text": "The department must black out private details like phone numbers and bank account info under Section 10, but still release the rest of the document."
+ },
+ {
+ "complex_text": "The respondent authority is directed to refund the excess demand of Rs. 650 collected from the appellant toward inspection charges, as the first hour of inspection is legally free of cost.",
+ "simple_text": "The office must refund Rs. 650 to you because the first hour of reviewing official records in person is completely free."
+ },
+ {
+ "complex_text": "The information sought relates to a dispute pending adjudication before a competent Court of Law, and disclosure has been expressly forbidden by judicial injunction under Section 8(1)(b).",
+ "simple_text": "The department cannot provide this information because a court judge has explicitly banned its public release during a trial."
+ },
+ {
+ "complex_text": "The Commission awards compensation of Rs. 5,000 to the appellant under Section 19(8)(b) for the harassment and detriment suffered due to misleading information provided by the CPIO.",
+ "simple_text": "The Information Commission awarded you Rs. 5,000 in compensation because the department caused you harassment by giving misleading replies."
+ },
+ {
+ "complex_text": "The CPIO is cautioned to remain diligent in adhering to the statutory timelines prescribed under the RTI Act, failing which disciplinary action will be recommended.",
+ "simple_text": "The officer received a formal warning to answer future RTI requests on time, or they will face departmental disciplinary penalties."
+ },
+ {
+ "complex_text": "The appellant is advised that grievances regarding non-receipt of pension cannot be resolved through RTI; the applicant should approach the Centralized Public Grievance Redress and Monitoring System (CPGRAMS).",
+ "simple_text": "RTI only provides government records and cannot directly fix a delayed pension. You should lodge a service complaint on the CPGRAMS grievance portal instead."
+ },
+ {
+ "complex_text": "The respondent shall provide the certified copy of the technical evaluation committee report along with comparative bid statements within two weeks of this order.",
+ "simple_text": "The department must provide certified copies of the contractor selection report and price comparisons within two weeks."
+ },
+ {
+ "complex_text": "Information received in confidence from a foreign government is strictly protected from disclosure under Section 8(1)(f) of the RTI Act.",
+ "simple_text": "Confidential documents received from a foreign nation or international body cannot be shared with the public."
+ },
+ {
+ "complex_text": "The CPIO is directed to verify the dispatch registers and tracking reports from the Department of Posts to establish whether the RTI response was actually mailed on the claimed date.",
+ "simple_text": "The officer must produce postal tracking receipts to prove that the reply letter was actually sent to you on the date they claimed."
+ },
+ {
+ "complex_text": "The public authority must intimate the calculation methodology for fees charged under Rule 4, clearly indicating page count, unit price, and mode of acceptable payment.",
+ "simple_text": "The department must clearly explain how they calculated your fees, stating the exact number of pages, rate per page, and payment options."
+ },
+ {
+ "complex_text": "Where the requested information is held by an intelligence or security organization specified in the Second Schedule, the provisions of the RTI Act do not apply as per Section 24, except in allegations of corruption and human rights violations.",
+ "simple_text": "Security and intelligence agencies (like IB or RAW) are exempt from RTI under Section 24, unless your request involves direct corruption or human rights abuses."
+ },
+ {
+ "complex_text": "The First Appellate Authority disposed of the first appeal ex-parte without according an opportunity of hearing to the appellant, violating the principles of natural justice.",
+ "simple_text": "The senior appeals officer improperly closed your appeal without letting you speak or present your side of the case."
+ },
+ {
+ "complex_text": "The public authority cannot withhold information merely because the file is marked 'Confidential' internally, unless it squarely falls within the exemptions listed under Section 8.",
+ "simple_text": "An office cannot hide a file just because it stamped it 'Confidential'. It must legally prove that one of the specific exemptions under Section 8 applies."
+ },
+ {
+ "complex_text": "The Commission directs the Registry to initiate penal proceedings against the deemed CPIO who failed to comply with previous orders despite repeated reminders.",
+ "simple_text": "The Commission has started penalty proceedings against the responsible officer who ignored earlier legal orders to release the records."
+ }
+]
\ No newline at end of file
diff --git a/pdf_simplifier_module/data/val_pairs.json b/pdf_simplifier_module/data/val_pairs.json
new file mode 100644
index 0000000000000000000000000000000000000000..687bc4d78099d09e2f85c03c5942ca1da06ca8d9
--- /dev/null
+++ b/pdf_simplifier_module/data/val_pairs.json
@@ -0,0 +1,26 @@
+[
+ {
+ "complex_text": "Any person aggrieved by a decision of the Public Information Officer may prefer an appeal under Section 19(1) of the RTI Act within thirty days from the expiry of the prescribed period.",
+ "simple_text": "If you are unhappy with the officer's reply or received no response, you can file a First Appeal within 30 days under Section 19(1)."
+ },
+ {
+ "complex_text": "Under Section 8(1)(a), information disclosure prejudicially affecting the sovereignty and integrity of India, the security, or strategic interests of the State is exempted from disclosure.",
+ "simple_text": "Information cannot be shared if releasing it could harm national security, strategic interests, or the sovereignty of India."
+ },
+ {
+ "complex_text": "The CPIO is directed to provide information regarding the total expenditure incurred on foreign official visits of the Vice Chancellor along with sanctioned approvals.",
+ "simple_text": "The university must disclose the total amount spent on the Vice Chancellor's overseas official trips along with copies of government approvals."
+ },
+ {
+ "complex_text": "The applicant cannot compel the public authority to create new data, answer hypothetical questions, or interpret legal provisions under the guise of seeking information.",
+ "simple_text": "Under RTI, an office is only required to give you existing files. They do not have to answer hypothetical 'what-if' questions or give legal opinions."
+ },
+ {
+ "complex_text": "The CPIO is directed to re-examine the third-party notice under Section 11 and make a speaking order detailing whether public interest outweighs individual objections.",
+ "simple_text": "The officer must review third-party objections under Section 11 and give a written decision on whether public interest requires releasing the files."
+ },
+ {
+ "complex_text": "The public authority shall facilitate electronic inspection of digitised land revenue records without charging search fees for the first sixty minutes.",
+ "simple_text": "The land revenue office must let you view digital land records on their computer screen for free during the first hour."
+ }
+]
\ No newline at end of file
diff --git a/pdf_simplifier_module/main.py b/pdf_simplifier_module/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..b673ea73fd061ad03b7516173ca8c5bdb407cc0f
--- /dev/null
+++ b/pdf_simplifier_module/main.py
@@ -0,0 +1,14 @@
+import sys
+from src.inference import DocumentSimplifier
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print("Usage: python main.py ")
+ sys.exit(1)
+
+ pdf_file = sys.argv[1]
+ simplifier = DocumentSimplifier()
+ result = simplifier.simplify_pdf(pdf_file)
+
+ print("\n--- SIMPLIFIED CIVIC SUMMARY ---\n")
+ print(result)
diff --git a/pdf_simplifier_module/models/saved_simplifier/readme.txt b/pdf_simplifier_module/models/saved_simplifier/readme.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0bd16d2586eeaf4c16b63f6c5596e208c61e14f6
--- /dev/null
+++ b/pdf_simplifier_module/models/saved_simplifier/readme.txt
@@ -0,0 +1 @@
+# Auto-created after training (stores model weights)
diff --git a/pdf_simplifier_module/requirements.txt b/pdf_simplifier_module/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..18f7cb0a480fcfb34090e0258a296a28f2688a3d
--- /dev/null
+++ b/pdf_simplifier_module/requirements.txt
@@ -0,0 +1,5 @@
+fastapi>=0.110.0
+uvicorn>=0.28.0
+python-multipart>=0.0.9
+gradio_client>=0.17.0
+pymupdf>=1.24.0
\ No newline at end of file
diff --git a/pdf_simplifier_module/server.py b/pdf_simplifier_module/server.py
new file mode 100644
index 0000000000000000000000000000000000000000..095406ba36699f6bdd56af0cb893408e0ecd698f
--- /dev/null
+++ b/pdf_simplifier_module/server.py
@@ -0,0 +1,107 @@
+import os
+import tempfile
+from fastapi import FastAPI, File, HTTPException, UploadFile
+from gradio_client import Client, handle_file
+import pymupdf
+
+app = FastAPI(
+ title="Civic & RTI PDF Simplifier API",
+ description="Transforms bureaucratic RTI/civic PDF documents into plain-language summaries.",
+ version="1.0.0",
+)
+
+HF_SPACE_ID = "Ronak0/RC"
+
+# Connect to the live Gradio Space instance running your fine-tuned model
+try:
+ client = Client(HF_SPACE_ID)
+except Exception as e:
+ client = None
+ print(f"[WARNING] Could not initialize Gradio client connection: {e}")
+
+
+def extract_pdf_chunks(
+ pdf_path: str, chunk_size: int = 180, max_chunks: int = 6
+) -> list[str]:
+ doc = pymupdf.open(pdf_path)
+ collected_lines = []
+
+ for page in doc:
+ text = page.get_text("text")
+ lines = [
+ line.strip()
+ for line in text.split("\n")
+ if len(line.strip().split()) >= 5
+ ]
+ collected_lines.extend(lines)
+
+ full_text = " ".join(collected_lines)
+ words = full_text.split()
+
+ if not words:
+ return []
+
+ chunks = [
+ " ".join(words[i : i + chunk_size])
+ for i in range(0, len(words), chunk_size)
+ ]
+ return chunks[:max_chunks]
+
+
+@app.get("/")
+def root():
+ return {
+ "status": "online",
+ "service": "Civic & RTI PDF Simplifier API",
+ "connected_space": HF_SPACE_ID,
+ }
+
+
+@app.post("/simplify")
+async def simplify_pdf(file: UploadFile = File(...)):
+ if not file.filename.lower().endswith(".pdf"):
+ raise HTTPException(
+ status_code=400,
+ detail="Invalid file format. Please upload a valid PDF document.",
+ )
+
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
+ tmp.write(await file.read())
+ tmp_path = tmp.name
+
+ try:
+ # 1. First attempt: Pass PDF directly to your fine-tuned Space pipeline
+ if client is not None:
+ try:
+ raw_result = client.predict(
+ pdf_file=handle_file(tmp_path), api_name="/predict"
+ )
+ if raw_result and isinstance(raw_result, str):
+ points = [
+ p.strip() for p in raw_result.split("\n\n") if p.strip()
+ ]
+ return {"filename": file.filename, "simplified_points": points}
+ except Exception as err:
+ print(f"[INFO] Direct space call fallback: {err}")
+
+ # 2. Local fallback chunk extraction if direct space payload differs
+ chunks = extract_pdf_chunks(tmp_path)
+ if not chunks:
+ return {
+ "filename": file.filename,
+ "simplified_points": [
+ "No readable text could be extracted from this document."
+ ],
+ }
+
+ return {
+ "filename": file.filename,
+ "simplified_points": [
+ f"Extracted Section {i+1}: {chunk[:160]}..."
+ for i, chunk in enumerate(chunks)
+ ],
+ }
+
+ finally:
+ if os.path.exists(tmp_path):
+ os.remove(tmp_path)
\ No newline at end of file
diff --git a/pdf_simplifier_module/src/__init__.py b/pdf_simplifier_module/src/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3f5a12faa99758192ecc4ed3fc22c9249232e86
--- /dev/null
+++ b/pdf_simplifier_module/src/__init__.py
@@ -0,0 +1 @@
+
diff --git a/pdf_simplifier_module/src/dataset_builder.py b/pdf_simplifier_module/src/dataset_builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3f5a12faa99758192ecc4ed3fc22c9249232e86
--- /dev/null
+++ b/pdf_simplifier_module/src/dataset_builder.py
@@ -0,0 +1 @@
+
diff --git a/pdf_simplifier_module/src/fetch_govt_pdfs.py b/pdf_simplifier_module/src/fetch_govt_pdfs.py
new file mode 100644
index 0000000000000000000000000000000000000000..57ffa171531f86c4d05fa623ea730713b9816f62
--- /dev/null
+++ b/pdf_simplifier_module/src/fetch_govt_pdfs.py
@@ -0,0 +1,68 @@
+import os
+import re
+import requests
+from bs4 import BeautifulSoup
+from urllib.parse import urljoin, urlparse
+
+# Target legitimate portal pages hosting circulars/orders/acts
+GOVT_SOURCES = [
+ "https://cic.gov.in/archive-high-court-ruling",
+ # Add other target portal listing URLs here
+]
+
+RAW_DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "raw")
+os.makedirs(RAW_DATA_DIR, exist_ok=True)
+
+HEADERS = {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+}
+
+def sanitize_filename(url: str) -> str:
+ path = urlparse(url).path
+ filename = os.path.basename(path)
+ if not filename.endswith(".pdf"):
+ filename += ".pdf"
+ # Remove invalid filename characters
+ return re.sub(r'[^a-zA-Z0-9_\-\.]', '_', filename)
+
+def download_pdf(url: str, output_path: str) -> bool:
+ try:
+ response = requests.get(url, headers=HEADERS, timeout=20, stream=True)
+ if response.status_code == 200 and "%PDF" in response.text[:10]:
+ with open(output_path, "wb") as f:
+ for chunk in response.iter_content(chunk_size=8192):
+ f.write(chunk)
+ print(f"[DOWNLOADED] {output_path}")
+ return True
+ except Exception as e:
+ print(f"[ERROR] Failed to fetch {url}: {e}")
+ return False
+
+def sync_government_pdfs(max_docs_per_source: int = 5):
+ for base_url in GOVT_SOURCES:
+ print(f"\nScanning: {base_url} ...")
+ try:
+ res = requests.get(base_url, headers=HEADERS, timeout=15)
+ soup = BeautifulSoup(res.text, "html.parser")
+
+ downloaded = 0
+ for link in soup.find_all("a", href=True):
+ href = link["href"]
+ if ".pdf" in href.lower():
+ full_pdf_url = urljoin(base_url, href)
+ file_name = sanitize_filename(full_pdf_url)
+ target_file = os.path.join(RAW_DATA_DIR, file_name)
+
+ if not os.path.exists(target_file):
+ if download_pdf(full_pdf_url, target_file):
+ downloaded += 1
+ else:
+ print(f"[EXISTS] {file_name} already in data/raw/")
+
+ if downloaded >= max_docs_per_source:
+ break
+ except Exception as e:
+ print(f"[ERROR] Scraper failed on {base_url}: {e}")
+
+if __name__ == "__main__":
+ sync_government_pdfs()
diff --git a/pdf_simplifier_module/src/inference.py b/pdf_simplifier_module/src/inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ced5543d70d04b256c3000e3e5a2c8aa97b851f
--- /dev/null
+++ b/pdf_simplifier_module/src/inference.py
@@ -0,0 +1,40 @@
+from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
+from src.pdf_extractor import extract_pdf_chunks
+import torch
+
+class DocumentSimplifier:
+ def __init__(self, model_path: str = "./models/saved_simplifier"):
+ self.tokenizer = AutoTokenizer.from_pretrained(model_path)
+ self.model = AutoModelForSeq2SeqLM.from_pretrained(model_path)
+ self.model.eval()
+
+ def simplify_pdf(self, pdf_path: str) -> str:
+ chunks = extract_pdf_chunks(pdf_path)
+
+ if not chunks:
+ return "No extractable text found in this PDF. It might be a pure scanned image without an OCR layer."
+
+ simplified_paragraphs = []
+ print(f"\nProcessing {len(chunks)} text section(s)...")
+
+ for idx, chunk in enumerate(chunks, 1):
+ prompt = f"simplify bureaucratic text into simple language: {chunk}"
+ inputs = self.tokenizer(prompt, return_tensors="pt", max_length=512, truncation=True)
+
+ with torch.no_grad():
+ output_tokens = self.model.generate(
+ **inputs,
+ max_length=120,
+ min_length=20,
+ num_beams=4,
+ no_repeat_ngram_size=3, # Blocks 3-word repeating loops
+ repetition_penalty=2.5, # Strongly penalizes token reuse
+ length_penalty=1.0,
+ early_stopping=True
+ )
+
+ summary = self.tokenizer.decode(output_tokens[0], skip_special_tokens=True)
+ simplified_paragraphs.append(f"• {summary}")
+ print(f" └── Completed section {idx}/{len(chunks)}")
+
+ return "\n\n".join(simplified_paragraphs)
\ No newline at end of file
diff --git a/pdf_simplifier_module/src/pdf_extractor.py b/pdf_simplifier_module/src/pdf_extractor.py
new file mode 100644
index 0000000000000000000000000000000000000000..18ed6086c56134aadb42e0b425f6970b27564fb7
--- /dev/null
+++ b/pdf_simplifier_module/src/pdf_extractor.py
@@ -0,0 +1,26 @@
+import pymupdf
+import re
+
+def extract_pdf_chunks(pdf_path: str, chunk_size: int = 200, max_chunks: int = 5) -> list[str]:
+ doc = pymupdf.open(pdf_path)
+ clean_paragraphs = []
+
+ for page in doc:
+ text = page.get_text("text")
+
+ # Split into logical lines/paragraphs
+ lines = [line.strip() for line in text.split("\n") if line.strip()]
+ for line in lines:
+ # Skip short index table rows, page numbers, and headers
+ if len(line.split()) < 8 or re.search(r'(\bPage\b|\bS\.No\b|\bLPA NO\b|\bWP\(C\)\b)', line, re.IGNORECASE):
+ continue
+ clean_paragraphs.append(line)
+
+ full_text = " ".join(clean_paragraphs)
+ words = full_text.split()
+
+ if not words:
+ return []
+
+ chunks = [" ".join(words[i:i + chunk_size]) for i in range(0, len(words), chunk_size)]
+ return chunks[:max_chunks]
\ No newline at end of file
diff --git a/pdf_simplifier_module/src/train.py b/pdf_simplifier_module/src/train.py
new file mode 100644
index 0000000000000000000000000000000000000000..32e9d5922d2a28a795450d1df85986ae5b4e598a
--- /dev/null
+++ b/pdf_simplifier_module/src/train.py
@@ -0,0 +1,61 @@
+from transformers import (
+ AutoTokenizer,
+ AutoModelForSeq2SeqLM,
+ Seq2SeqTrainer,
+ Seq2SeqTrainingArguments,
+ DataCollatorForSeq2Seq
+)
+from datasets import load_dataset
+import os
+
+def main():
+ model_id = "google/flan-t5-base"
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
+ model = AutoModelForSeq2SeqLM.from_pretrained(model_id)
+
+ dataset = load_dataset("json", data_files={
+ "train": "data/train_pairs.json",
+ "validation": "data/val_pairs.json"
+ })
+
+ prefix = "simplify bureaucratic text into simple language: "
+
+ def preprocess(examples):
+ inputs = [prefix + doc for doc in examples["complex_text"]]
+ model_inputs = tokenizer(inputs, max_length=512, truncation=True)
+ labels = tokenizer(text_target=examples["simple_text"], max_length=128, truncation=True)
+ model_inputs["labels"] = labels["input_ids"]
+ return model_inputs
+
+ tokenized_data = dataset.map(preprocess, batched=True)
+
+ os.makedirs("./models/saved_simplifier", exist_ok=True)
+
+ training_args = Seq2SeqTrainingArguments(
+ output_dir="./models/checkpoints",
+ eval_strategy="epoch",
+ learning_rate=5e-5,
+ per_device_train_batch_size=2,
+ per_device_eval_batch_size=2,
+ num_train_epochs=3,
+ weight_decay=0.01,
+ save_strategy="epoch",
+ logging_steps=5
+ )
+
+ trainer = Seq2SeqTrainer(
+ model=model,
+ args=training_args,
+ train_dataset=tokenized_data["train"],
+ eval_dataset=tokenized_data["validation"],
+ processing_class=tokenizer,
+ data_collator=DataCollatorForSeq2Seq(tokenizer, model=model)
+ )
+
+ trainer.train()
+ model.save_pretrained("./models/saved_simplifier")
+ tokenizer.save_pretrained("./models/saved_simplifier")
+ print("\n[SUCCESS] Model weights saved to ./models/saved_simplifier")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/pdf_simplifier_module/upload_weights.py b/pdf_simplifier_module/upload_weights.py
new file mode 100644
index 0000000000000000000000000000000000000000..9a12e6d36195a8d74c3286dbe471f82488db574e
--- /dev/null
+++ b/pdf_simplifier_module/upload_weights.py
@@ -0,0 +1,24 @@
+import os
+from huggingface_hub import HfApi
+from transformers import AutoConfig, AutoModelForSeq2SeqLM
+
+LOCAL_DIR = "./models/saved_simplifier"
+REPO_ID = "Ronak0/rti-pdf-simplifier"
+
+print("1. Optimizing weights to float16...")
+model = AutoModelForSeq2SeqLM.from_pretrained(LOCAL_DIR).half()
+model.save_pretrained(LOCAL_DIR)
+
+print("2. Generating valid configuration...")
+config = AutoConfig.from_pretrained("google/flan-t5-base")
+config.save_pretrained(LOCAL_DIR)
+
+print(f"3. Uploading to Hugging Face Model Hub ({REPO_ID})...")
+api = HfApi()
+api.create_repo(repo_id=REPO_ID, repo_type="model", exist_ok=True)
+api.upload_folder(
+ folder_path=LOCAL_DIR,
+ repo_id=REPO_ID,
+ repo_type="model",
+)
+print("Upload completed successfully.")
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8ca30896b02e3cc9f436b4b4b56354c3a4115b13
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+streamlit>=1.35.0
+transformers>=4.40.0
+torch
+sentencepiece>=0.2.0
+protobuf>=3.20.0
+pymupdf>=1.24.0
\ No newline at end of file
diff --git a/rights_navigator/database/chroma_db/chroma.sqlite3 b/rights_navigator/database/chroma_db/chroma.sqlite3
new file mode 100644
index 0000000000000000000000000000000000000000..fc444f533ddc77931f5da67ae3173b1aa68dd28c
--- /dev/null
+++ b/rights_navigator/database/chroma_db/chroma.sqlite3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cfd49d259a81e879814545014714e28bcf28e326f2ee0475e81c09a9bf17bc44
+size 188416
diff --git a/rights_navigator/rights_navigator.py b/rights_navigator/rights_navigator.py
new file mode 100644
index 0000000000000000000000000000000000000000..39e2e3c12c10de60bc16d83ab7704dab287a9a23
--- /dev/null
+++ b/rights_navigator/rights_navigator.py
@@ -0,0 +1,1045 @@
+from pathlib import Path
+
+import chromadb
+import torch
+
+from sentence_transformers import SentenceTransformer, CrossEncoder
+from transformers import (
+ AutoTokenizer,
+ AutoModelForCausalLM,
+ BitsAndBytesConfig
+)
+
+
+# ============================================================
+# PATHS
+# ============================================================
+
+# rights_navigator.py is inside:
+# oosc/rights_navigator/
+#
+# Therefore:
+# .parent -> oosc/rights_navigator
+# .parent.parent -> oosc
+
+PROJECT_ROOT = Path(__file__).resolve().parent.parent
+
+CHROMA_PATH = PROJECT_ROOT / "database" / "chroma_db"
+
+
+# ============================================================
+# EMBEDDING MODEL
+# ============================================================
+
+print("Loading embedding model...")
+
+embedding_model = SentenceTransformer(
+ "sentence-transformers/all-MiniLM-L6-v2"
+)
+
+print("Embedding model loaded.")
+
+
+# ============================================================
+# RERANKER
+# ============================================================
+
+print("Loading reranker...")
+
+reranker = CrossEncoder(
+ "cross-encoder/ms-marco-MiniLM-L-6-v2"
+)
+
+print("Reranker loaded.")
+
+
+# ============================================================
+# QWEN 2.5 3B - 4 BIT QUANTIZED
+# ============================================================
+
+LLM_MODEL = "Qwen/Qwen2.5-3B-Instruct"
+
+print("Loading Qwen model in 4-bit...")
+
+tokenizer = AutoTokenizer.from_pretrained(
+ LLM_MODEL
+)
+
+quantization_config = BitsAndBytesConfig(
+ load_in_4bit=True,
+ bnb_4bit_quant_type="nf4",
+ bnb_4bit_compute_dtype=torch.float16,
+ bnb_4bit_use_double_quant=True
+)
+
+llm_model = AutoModelForCausalLM.from_pretrained(
+ LLM_MODEL,
+ quantization_config=quantization_config,
+ device_map="auto",
+ dtype=torch.float16
+)
+
+print("Qwen 4-bit model loaded.")
+
+
+# ============================================================
+# CHROMADB
+# ============================================================
+
+print("Connecting to ChromaDB...")
+
+client = chromadb.PersistentClient(
+ path=str(CHROMA_PATH)
+)
+
+collection = client.get_collection(
+ name="legal_documents"
+)
+
+print("ChromaDB connected.")
+
+
+# ============================================================
+# DOMAIN KEYWORDS
+# ============================================================
+
+consumer_keywords = [
+ "seller",
+ "shopkeeper",
+ "shop",
+ "product",
+ "goods",
+ "purchase",
+ "refund",
+ "replacement",
+ "defective",
+ "defect",
+ "damaged",
+ "consumer",
+ "customer",
+ "order",
+ "delivery",
+ "delivered",
+ "warranty",
+ "price",
+ "money back",
+ "wrong product",
+ "different product",
+ "different item",
+ "online shopping",
+ "ecommerce",
+ "e-commerce",
+ "service provider",
+ "misleading advertisement"
+]
+
+
+tenancy_keywords = [
+ "landlord",
+ "tenant",
+ "rent",
+ "rental",
+ "lease",
+ "rent agreement",
+ "rental agreement",
+ "security deposit",
+ "deposit",
+ "eviction",
+ "rented house",
+ "rented flat",
+ "tenancy",
+ "tenancy agreement",
+ "rent increase",
+ "rent payment",
+ "tenant rights",
+ "rental property"
+]
+
+
+workplace_keywords = [
+ "employer",
+ "employee",
+ "salary",
+ "wages",
+ "wage",
+ "job",
+ "workplace",
+ "worker",
+ "labour",
+ "labor",
+ "employment",
+ "dismissed",
+ "termination",
+ "terminated",
+ "fired",
+ "layoff",
+ "laid off",
+ "retrenchment",
+ "strike",
+ "union",
+ "trade union",
+ "industrial dispute",
+ "bonus",
+ "overtime",
+ "employment contract",
+ "wrongful termination"
+]
+
+
+# ============================================================
+# MAIN FUNCTION
+# ============================================================
+
+def get_rights_response(query: str):
+
+ query = query.strip()
+
+ if not query:
+ return {
+ "success": False,
+ "error": "Grievance cannot be empty."
+ }
+
+ print("\n========================================")
+ print("RIGHTS NAVIGATOR")
+ print("========================================")
+ print("Grievance:", query)
+
+ query_lower = query.lower()
+
+
+ # ========================================================
+ # DOMAIN DETECTION
+ # ========================================================
+
+ print("\nDetecting legal domain...")
+
+ domain_scores = {
+ "consumer": 0,
+ "tenancy": 0,
+ "workplace": 0
+ }
+
+
+ for keyword in consumer_keywords:
+
+ if keyword in query_lower:
+ domain_scores["consumer"] += 1
+
+
+ for keyword in tenancy_keywords:
+
+ if keyword in query_lower:
+ domain_scores["tenancy"] += 1
+
+
+ for keyword in workplace_keywords:
+
+ if keyword in query_lower:
+ domain_scores["workplace"] += 1
+
+
+ # Strong tenancy indicators
+
+ if (
+ "landlord" in query_lower
+ or "tenant" in query_lower
+ ):
+ domain_scores["tenancy"] += 5
+
+
+ # Strong workplace indicators
+
+ if (
+ "employer" in query_lower
+ or "employee" in query_lower
+ ):
+ domain_scores["workplace"] += 5
+
+
+ # Strong consumer indicators
+
+ if (
+ "seller" in query_lower
+ or "defective product" in query_lower
+ or "refund" in query_lower
+ or "wrong product" in query_lower
+ ):
+ domain_scores["consumer"] += 5
+
+
+ selected_domain = max(
+ domain_scores,
+ key=domain_scores.get
+ )
+
+ highest_score = domain_scores[selected_domain]
+
+
+ print("Detected domain:", selected_domain.upper())
+
+
+ # ========================================================
+ # UNKNOWN DOMAIN
+ # ========================================================
+
+ if highest_score == 0:
+
+ print("No supported legal domain detected.")
+
+ return {
+ "success": True,
+ "domain": "unknown",
+ "answer": (
+ "The available legal material does not clearly cover "
+ "this type of grievance."
+ ),
+ "sources": []
+ }
+
+
+ # ========================================================
+ # RETRIEVAL CONCEPTS
+ # ========================================================
+
+ if selected_domain == "consumer":
+
+ retrieval_concepts = """
+ consumer protection
+ seller
+ buyer
+ consumer
+ goods
+ defective goods
+ defective product
+ damaged product
+ wrong product
+ different product
+ product not as ordered
+ goods not matching description
+ refund
+ replacement
+ return of price
+ compensation
+ consumer complaint
+ unfair trade practice
+ deficiency in service
+ District Commission
+ """
+
+
+ elif selected_domain == "tenancy":
+
+ retrieval_concepts = """
+ tenancy
+ tenant
+ landlord
+ rent
+ rental agreement
+ lease
+ security deposit
+ rent deposit
+ eviction
+ rental property
+ residential premises
+ tenancy rights
+ landlord obligations
+ tenant obligations
+ rent dispute
+ """
+
+
+ else:
+
+ retrieval_concepts = """
+ workplace
+ employer
+ employee
+ worker
+ wages
+ salary
+ employment
+ industrial dispute
+ labour
+ labor
+ termination
+ dismissal
+ retrenchment
+ strike
+ trade union
+ employment dispute
+ worker rights
+ """
+
+
+ # ========================================================
+ # RETRIEVAL QUERY
+ # ========================================================
+
+ retrieval_query = f"""
+ Legal dispute in the {selected_domain} domain:
+
+ USER GRIEVANCE:
+ {query}
+
+ RELEVANT LEGAL CONCEPTS:
+ {retrieval_concepts}
+ """
+
+
+ # ========================================================
+ # EMBEDDING
+ # ========================================================
+
+ print("\nGenerating query embedding...")
+
+ query_embedding = embedding_model.encode(
+ retrieval_query
+ )
+
+ print("Query embedding generated.")
+
+
+ # ========================================================
+ # CHROMADB SEARCH
+ # ========================================================
+
+ print("\nSearching legal database...")
+
+ results = collection.query(
+ query_embeddings=[
+ query_embedding.tolist()
+ ],
+ n_results=10,
+ where={
+ "category": selected_domain
+ }
+ )
+
+ print("ChromaDB search completed.")
+
+
+ documents = results["documents"][0]
+ metadatas = results["metadatas"][0]
+
+
+ if len(documents) == 0:
+
+ print("No legal documents found.")
+
+ return {
+ "success": True,
+ "domain": selected_domain,
+ "answer": (
+ "No relevant legal material was found for "
+ "this grievance."
+ ),
+ "sources": []
+ }
+
+
+ print("Retrieved sections:", len(documents))
+
+
+ # ========================================================
+ # CROSS-ENCODER RERANKING
+ # ========================================================
+
+ print("\nReranking legal sections...")
+
+ pairs = [
+ (query, document)
+ for document in documents
+ ]
+
+
+ rerank_scores = reranker.predict(
+ pairs
+ )
+
+ print("Reranking completed.")
+
+
+ # ========================================================
+ # LEGAL BOOSTING
+ # ========================================================
+
+ ranked_results = []
+
+
+ for score, document, metadata in zip(
+ rerank_scores,
+ documents,
+ metadatas
+ ):
+
+ text_lower = document.lower()
+
+ legal_boost = 0.0
+
+
+ # ====================================================
+ # CONSUMER
+ # ====================================================
+
+ if selected_domain == "consumer":
+
+ if any(
+ word in query_lower
+ for word in [
+ "refund",
+ "return",
+ "money back"
+ ]
+ ):
+
+ if "return to the complainant the price" in text_lower:
+ legal_boost += 5.0
+
+ if "replace the goods" in text_lower:
+ legal_boost += 4.0
+
+
+ if any(
+ word in query_lower
+ for word in [
+ "defective",
+ "defect",
+ "damaged"
+ ]
+ ):
+
+ if "defects" in text_lower:
+ legal_boost += 3.0
+
+ if "defective product" in text_lower:
+ legal_boost += 3.0
+
+
+ if any(
+ word in query_lower
+ for word in [
+ "different product",
+ "wrong product",
+ "not as ordered",
+ "different item"
+ ]
+ ):
+
+ if "goods sold or delivered" in text_lower:
+ legal_boost += 4.0
+
+ if "replace the goods" in text_lower:
+ legal_boost += 4.0
+
+ if "return to the complainant the price" in text_lower:
+ legal_boost += 4.0
+
+
+ if any(
+ word in query_lower
+ for word in [
+ "complaint",
+ "file a complaint",
+ "consumer commission"
+ ]
+ ):
+
+ if "complaint" in text_lower:
+ legal_boost += 2.0
+
+
+ # ====================================================
+ # TENANCY
+ # ====================================================
+
+ elif selected_domain == "tenancy":
+
+ if any(
+ word in query_lower
+ for word in [
+ "deposit",
+ "security deposit"
+ ]
+ ):
+
+ if "security deposit" in text_lower:
+ legal_boost += 5.0
+
+ elif "deposit" in text_lower:
+ legal_boost += 3.0
+
+
+ if (
+ "rent" in query_lower
+ or "rental" in query_lower
+ ):
+
+ if "rent" in text_lower:
+ legal_boost += 3.0
+
+
+ if "landlord" in query_lower:
+
+ if "landlord" in text_lower:
+ legal_boost += 3.0
+
+
+ if "tenant" in query_lower:
+
+ if "tenant" in text_lower:
+ legal_boost += 3.0
+
+
+ if "eviction" in query_lower:
+
+ if "eviction" in text_lower:
+ legal_boost += 5.0
+
+
+ # ====================================================
+ # WORKPLACE
+ # ====================================================
+
+ elif selected_domain == "workplace":
+
+ if any(
+ word in query_lower
+ for word in [
+ "salary",
+ "wages",
+ "wage"
+ ]
+ ):
+
+ if "wages" in text_lower:
+ legal_boost += 5.0
+
+ if "salary" in text_lower:
+ legal_boost += 5.0
+
+
+ if any(
+ word in query_lower
+ for word in [
+ "termination",
+ "terminated",
+ "dismissed",
+ "fired",
+ "retrenchment",
+ "layoff",
+ "laid off"
+ ]
+ ):
+
+ if "termination" in text_lower:
+ legal_boost += 4.0
+
+ if "retrenchment" in text_lower:
+ legal_boost += 4.0
+
+ if "dismissed" in text_lower:
+ legal_boost += 3.0
+
+
+ if "strike" in query_lower:
+
+ if "strike" in text_lower:
+ legal_boost += 5.0
+
+
+ if (
+ "union" in query_lower
+ or "trade union" in query_lower
+ ):
+
+ if "union" in text_lower:
+ legal_boost += 4.0
+
+
+ if any(
+ word in query_lower
+ for word in [
+ "employer",
+ "employee",
+ "worker"
+ ]
+ ):
+
+ if "employer" in text_lower:
+ legal_boost += 2.0
+
+ if "employee" in text_lower:
+ legal_boost += 2.0
+
+ if "worker" in text_lower:
+ legal_boost += 2.0
+
+
+ # ====================================================
+ # FINAL SCORE
+ # ====================================================
+
+ final_score = (
+ float(score)
+ + legal_boost
+ )
+
+
+ ranked_results.append(
+ (
+ final_score,
+ float(score),
+ legal_boost,
+ document,
+ metadata
+ )
+ )
+
+
+ # ========================================================
+ # SORT
+ # ========================================================
+
+ ranked_results.sort(
+ key=lambda x: x[0],
+ reverse=True
+ )
+
+
+ # ========================================================
+ # TOP 3
+ # ========================================================
+
+ ranked_results = ranked_results[:3]
+
+
+ print("\nTop legal sections:")
+
+ for i, (
+ final_score,
+ original_score,
+ legal_boost,
+ document,
+ metadata
+ ) in enumerate(ranked_results):
+
+ print(
+ f"#{i + 1} | "
+ f"Section {metadata['section']} | "
+ f"Page {metadata['page']} | "
+ f"Score {final_score:.4f}"
+ )
+
+
+ # ========================================================
+ # BUILD LEGAL CONTEXT
+ # ========================================================
+
+ print("\nBuilding legal context...")
+
+ context_parts = []
+ sources = []
+
+
+ for i, (
+ final_score,
+ original_score,
+ legal_boost,
+ document,
+ metadata
+ ) in enumerate(ranked_results):
+
+ context_parts.append(
+ f"""
+SOURCE {i + 1}
+
+ACT:
+{metadata['act']}
+
+CATEGORY:
+{metadata['category']}
+
+SECTION:
+{metadata['section']}
+
+TITLE:
+{metadata['title']}
+
+PAGE:
+{metadata['page']}
+
+LEGAL TEXT:
+{document}
+
+END SOURCE {i + 1}
+"""
+ )
+
+
+ sources.append(
+ {
+ "act": metadata["act"],
+ "section": metadata["section"],
+ "title": metadata["title"],
+ "page": metadata["page"]
+ }
+ )
+
+
+ context = "\n".join(context_parts)
+
+ print("Legal context ready.")
+
+
+ # ========================================================
+ # PROMPT
+ # ========================================================
+
+ prompt = f"""
+You are Rights Navigator, an Indian legal information assistant.
+
+Detected legal domain:
+{selected_domain}
+
+USER GRIEVANCE:
+{query}
+
+
+==================================================
+LEGAL CONTEXT
+==================================================
+
+{context}
+
+
+==================================================
+STRICT EVIDENCE RULES
+==================================================
+
+You MUST answer using ONLY the legal material in the
+LEGAL CONTEXT.
+
+Do not use outside legal knowledge.
+
+Do not invent:
+
+- legal sections
+- legal rights
+- remedies
+- deadlines
+- authorities
+- penalties
+- procedures
+- appeal mechanisms
+
+Every legal claim MUST be supported by the exact
+section whose legal text contains that claim.
+
+Do not combine different sections and attribute their
+contents to one section.
+
+If the context does not support a claim, do not make
+that claim.
+
+This is legal information, not personalized legal
+representation.
+
+
+==================================================
+DOMAIN RULE
+==================================================
+
+Only discuss the detected domain:
+
+{selected_domain}
+
+Do not introduce laws from another domain.
+
+
+==================================================
+IMPORTANT EVIDENCE RULE
+==================================================
+
+Prefer substantive legal provisions over procedural
+provisions.
+
+A section that provides a refund or replacement is
+a substantive remedy.
+
+A section that only explains how a complaint is filed
+is a procedural provision.
+
+Do not present a procedural provision as a legal remedy.
+
+
+==================================================
+OUTPUT FORMAT
+==================================================
+
+Provide EXACTLY these three sections:
+
+### 1. YOUR LEGAL RIGHTS
+
+Give maximum 3 concise bullet points.
+
+For every important legal claim, mention:
+
+Act + Section.
+
+
+### 2. IMMEDIATE ACTIONABLE STEPS
+
+Give maximum 4 numbered steps.
+
+Only provide steps directly supported by the legal
+material.
+
+Do not invent deadlines, forms, authorities or
+procedures.
+
+
+### 3. WHERE TO ESCALATE
+
+Mention an escalation authority ONLY if the retrieved
+legal material explicitly contains an escalation rule.
+
+Do not infer an escalation authority from general
+legal knowledge.
+
+If the context does not explicitly identify an
+escalation mechanism, output exactly:
+
+Insufficient information in the retrieved legal material
+to determine the escalation authority.
+
+
+==================================================
+STYLE
+==================================================
+
+Keep the answer concise and practical.
+
+Do not reproduce the legal text.
+
+Summarize legal provisions in your own words.
+
+Do not quote long passages.
+
+Do not repeat the same legal claim.
+
+Do not hallucinate.
+
+Do not guess.
+"""
+
+
+ # ========================================================
+ # GENERATE WITH QWEN
+ # ========================================================
+
+ print("\nGenerating answer with Qwen...")
+ print("This may take some time because the model may use CPU/disk offloading.")
+
+
+ messages = [
+ {
+ "role": "system",
+ "content": (
+ "You are Rights Navigator, an Indian legal "
+ "information assistant. Use only the provided "
+ "legal context."
+ )
+ },
+ {
+ "role": "user",
+ "content": prompt
+ }
+ ]
+
+
+ # ========================================================
+ # CHAT TEMPLATE
+ # ========================================================
+
+ text = tokenizer.apply_chat_template(
+ messages,
+ tokenize=False,
+ add_generation_prompt=True
+ )
+
+
+ # ========================================================
+ # TOKENIZE
+ # ========================================================
+
+ model_inputs = tokenizer(
+ [text],
+ return_tensors="pt"
+ )
+
+
+ # ========================================================
+ # MOVE INPUT TO MODEL DEVICE
+ # ========================================================
+
+ model_inputs = {
+ key: value.to(llm_model.device)
+ for key, value in model_inputs.items()
+ }
+
+
+ # ========================================================
+ # GENERATION
+ # ========================================================
+
+ with torch.no_grad():
+
+ generated_ids = llm_model.generate(
+ **model_inputs,
+ max_new_tokens=250,
+ do_sample=False,
+ pad_token_id=tokenizer.eos_token_id
+ )
+
+
+ print("Qwen generation completed.")
+
+
+ # ========================================================
+ # REMOVE INPUT TOKENS
+ # ========================================================
+
+ generated_ids = [
+ output_ids[len(input_ids):]
+ for input_ids, output_ids
+ in zip(
+ model_inputs["input_ids"],
+ generated_ids
+ )
+ ]
+
+
+ # ========================================================
+ # DECODE
+ # ========================================================
+
+ answer = tokenizer.batch_decode(
+ generated_ids,
+ skip_special_tokens=True
+ )[0].strip()
+
+
+ # ========================================================
+ # RETURN
+ # ========================================================
+
+ print("Answer generated successfully.")
+
+
+ return {
+ "success": True,
+ "domain": selected_domain,
+ "answer": answer,
+ "sources": sources
+ }
\ No newline at end of file
diff --git a/rti_formatter_module/.gitignore b/rti_formatter_module/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/rti_formatter_module/__init__.py b/rti_formatter_module/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/rti_formatter_module/generator.py b/rti_formatter_module/generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..3eee166312e9b9787b287d3b608fbeb8d3329c02
--- /dev/null
+++ b/rti_formatter_module/generator.py
@@ -0,0 +1,22 @@
+from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
+import torch
+
+def generate_rti_draft(complaint_text: str, model_id: str = "CaseLoop/rti-formatter") -> str:
+ """Loads the fine-tuned T5 formatter from Hugging Face and generates a structured RTI draft."""
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
+ model = AutoModelForSeq2SeqLM.from_pretrained(model_id)
+
+ prompt = f"Format as RTI: {complaint_text}"
+ inputs = tokenizer(prompt, return_tensors="pt")
+
+ with torch.no_grad():
+ outputs = model.generate(
+ **inputs,
+ max_new_tokens=256,
+ num_beams=4,
+ no_repeat_ngram_size=3, # Prevents repeated loops
+ repetition_penalty=2.0, # Penalizes word/phrase repetition
+ early_stopping=True
+ )
+
+ return tokenizer.decode(outputs[0], skip_special_tokens=True)
\ No newline at end of file
diff --git a/rti_formatter_module/run_test.py b/rti_formatter_module/run_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..20d5cd14c4e479915ef1f231afcb49902e3a6b9d
--- /dev/null
+++ b/rti_formatter_module/run_test.py
@@ -0,0 +1,45 @@
+import sys
+from pathlib import Path
+
+# Ensure the root directory is on the path so module imports work correctly
+root_dir = Path(__file__).resolve().parent.parent
+if str(root_dir) not in sys.path:
+ sys.path.append(str(root_dir))
+
+from rti_formatter_module.generator import generate_rti_draft
+
+def test_formatter():
+ test_cases = [
+ (
+ "Road Infrastructure",
+ "There are massive potholes on MG Road near the metro station causing traffic jams and accidents since last month. No repairs have been done."
+ ),
+ (
+ "Water Supply Issue",
+ "Water supply has been completely cut off in Sector 4 residential area for the past 4 days without prior notice from the municipal corporation."
+ ),
+ (
+ "Garbage Disposal",
+ "Sanitation workers have not collected garbage from Ward 12 for two weeks, creating illegal open dumping sites near the school."
+ )
+ ]
+
+ print("=" * 60)
+ print("RUNNING RTI FORMATTER TEST SUITE")
+ print("=" * 60)
+
+ for idx, (category, grievance) in enumerate(test_cases, 1):
+ print(f"\n[Test Case {idx}] Category: {category}")
+ print(f"Input Grievance: {grievance}\n")
+
+ try:
+ draft = generate_rti_draft(grievance)
+ print("--- Output RTI Draft ---")
+ print(draft)
+ except Exception as e:
+ print(f"Error during generation: {e}")
+
+ print("-" * 60)
+
+if __name__ == "__main__":
+ test_formatter()
\ No newline at end of file