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 +

+
+ +
+ + {/* 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 */} +
+ + +
+
+
+ ); +}; 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

+
+
+ +
+ + {/* 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 +
+
+ +
+ + +
+
+ ) : ( + <> + {/* Option 1: Continue with Google */} + + +
+
+ + OR EMAIL LOGIN + +
+
+ + {/* Toggle between Sign in and Sign up */} +
+ + +
+ + {/* Option 2: Email Form */} +
+ {authMode === 'signup' && ( +
+ + setDisplayName(e.target.value)} + placeholder="e.g. John Doe" + className="w-full bg-white border border-[#CBD5E1] rounded-xl px-3.5 py-2.5 text-[14px] text-[#0F172A] outline-none focus:border-[#006c4a]" + /> +
+ )} + +
+ + setEmail(e.target.value)} + placeholder="name@example.com" + required + className="w-full bg-white border border-[#CBD5E1] rounded-xl px-3.5 py-2.5 text-[14px] text-[#0F172A] outline-none focus:border-[#006c4a]" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + required + minLength={6} + className="w-full bg-white border border-[#CBD5E1] rounded-xl px-3.5 py-2.5 text-[14px] text-[#0F172A] outline-none focus:border-[#006c4a]" + /> +
+ + +
+ + )} +
+ + {/* Footer */} +
+ Protected with Firebase Auth + +
+
+
+ ); +}; 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 */} +
+
+
+ + / + + / + {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}% +
+ + +
+
+ + {/* Step Tabs Navigation Bar */} +
+ {STEPS.map((step) => { + const isActive = activeStep === step.num; + return ( + + ); + })} +
+ + {/* 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}

+
+ +
+ )) + )} +
+
+ +
+ + 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'}. +

+
+ +
+ setSearchQuery(e.target.value)} + placeholder="e.g. BBMP Solid Waste, Consumer Forum, BESCOM, Police PIO" + className="flex-1 bg-white border border-[#CBD5E1] rounded-xl px-3 py-2 text-[13px] text-[#0F172A] outline-none focus:border-[#006c4a]" + /> + +
+ + {searchError && ( +
+ {searchError} +
+ )} + + {searchResult && ( +
+
+ {searchResult.department} + + Verified + +
+

{searchResult.officerTitle}

+

+ Email: {searchResult.nodalEmail} +

+

+ Address: {searchResult.officeAddress} +

+ {searchResult.grievancePortal && ( +

+ Portal: {searchResult.grievancePortal} +

+ )} + +
+ )} +
+
+
+ )} + + {/* 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. +

+
+ +
+ + {/* Add Fact Form */} + {showAddFact && ( +
+
+
+ + setNewFactLabel(e.target.value)} + placeholder="e.g. Transaction Ref / Incident Date / Disputed Amount" + className="w-full bg-white border border-[#CBD5E1] rounded-lg px-3 py-2 text-[13px] text-[#0F172A] outline-none focus:border-[#006c4a]" + required + /> +
+
+ + setNewFactValue(e.target.value)} + placeholder="e.g. ₹1,40,000 security deposit withheld despite clean exit handover on Jan 15" + className="w-full bg-white border border-[#CBD5E1] rounded-lg px-3 py-2 text-[13px] text-[#0F172A] outline-none focus:border-[#006c4a]" + required + /> +
+
+
+ + +
+
+ )} + +
+ {(currentCase.facts || []).map((fact) => ( +
+
+ + {fact.label} + +

{fact.value}

+
+ +
+ ))} +
+ +
+ + Specific Relief / Remedy Demanded: + +