khagu commited on
Commit
3998131
·
0 Parent(s):

chore: finally untrack large database files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +22 -0
  2. .env.example +1 -0
  3. .env.production.example +16 -0
  4. .github/workflows/ci.yml +48 -0
  5. .gitignore +103 -0
  6. Backend.Dockerfile +36 -0
  7. Backend/.env.example +1 -0
  8. Backend/core/config.py +17 -0
  9. Backend/core/deps.py +37 -0
  10. Backend/core/security.py +33 -0
  11. Backend/db/mongodb.py +10 -0
  12. Backend/main.py +42 -0
  13. Backend/requirements.txt +7 -0
  14. Backend/routes/auth.py +55 -0
  15. Backend/schemas/user.py +25 -0
  16. Frontend.Dockerfile +29 -0
  17. Frontend/.env.production.example +4 -0
  18. Frontend/.gitignore +29 -0
  19. Frontend/app/api/Bias_Chat/route.ts +30 -0
  20. Frontend/app/api/Legal_Chat/route.ts +97 -0
  21. Frontend/app/api/bias-detection-hitl/approve/route.ts +39 -0
  22. Frontend/app/api/bias-detection-hitl/generate-pdf/route.ts +53 -0
  23. Frontend/app/api/bias-detection-hitl/regenerate/route.ts +39 -0
  24. Frontend/app/api/bias-detection/route.ts +222 -0
  25. Frontend/app/api/letter-generation/route.ts +97 -0
  26. Frontend/app/api/login/route.ts +20 -0
  27. Frontend/app/bias-checker/page.tsx +47 -0
  28. Frontend/app/chatbot/page.tsx +45 -0
  29. Frontend/app/dashboard/page.tsx +323 -0
  30. Frontend/app/globals.css +160 -0
  31. Frontend/app/layout.tsx +48 -0
  32. Frontend/app/letter-generator/page.tsx +64 -0
  33. Frontend/app/loading.tsx +3 -0
  34. Frontend/app/login/page.tsx +131 -0
  35. Frontend/app/page.tsx +147 -0
  36. Frontend/app/profile/page.tsx +197 -0
  37. Frontend/app/register/page.tsx +17 -0
  38. Frontend/components.json +21 -0
  39. Frontend/components/auth/register-form.tsx +194 -0
  40. Frontend/components/chatbot/bias-checker.tsx +805 -0
  41. Frontend/components/chatbot/bias-checker.tsx.backup +213 -0
  42. Frontend/components/chatbot/law-chatbot.tsx +674 -0
  43. Frontend/components/chatbot/letter-generator.tsx +739 -0
  44. Frontend/components/common/file-upload.tsx +108 -0
  45. Frontend/components/dashboard/stats-card.tsx +37 -0
  46. Frontend/components/layout/footer.tsx +133 -0
  47. Frontend/components/layout/navbar.tsx +192 -0
  48. Frontend/components/theme-provider.tsx +11 -0
  49. Frontend/components/ui/accordion.tsx +66 -0
  50. Frontend/components/ui/alert-dialog.tsx +157 -0
.dockerignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exclude node modules and build artifacts from Docker build context
2
+ Frontend/node_modules
3
+ Frontend/.next
4
+ Frontend/.pnpm-store
5
+ **/node_modules
6
+ **/.next
7
+
8
+ # General ignores
9
+ .git
10
+ .gitignore
11
+ .vscode
12
+ __pycache__/
13
+ *.pyc
14
+ *.pyo
15
+ *.pyd
16
+ dist/
17
+ build/
18
+ .DS_Store
19
+
20
+ # env ignore
21
+ .env
22
+ .env.local
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ MISTRAL_API_KEY=your_api_key_here
.env.production.example ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Backend Production Environment Variables ---
2
+
3
+ # Mistral AI API Key (Required for RAG)
4
+ MISTRAL_API_KEY=your_mistral_api_key_here
5
+
6
+ # Supabase Configuration (Required for Auth and Chat History)
7
+ SUPABASE_URL=https://your-project-id.supabase.co
8
+ SUPABASE_ANON_KEY=your-anon-key
9
+ SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
10
+
11
+ # JWT Configuration
12
+ JWT_SECRET=your-super-secret-key-change-this
13
+ JWT_ALGORITHM=HS256
14
+
15
+ # Optional: Port (Defaults to 7860 in Dockerfile)
16
+ # PORT=7860
.github/workflows/ci.yml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: '**'
6
+ pull_request:
7
+ branches: '**'
8
+
9
+ jobs:
10
+ backend-tests:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v4
17
+ with:
18
+ python-version: '3.11'
19
+
20
+ - name: Install dependencies
21
+ run: |
22
+ python -m pip install --upgrade pip
23
+ pip install -r requirements.txt
24
+ if [ -f Backend/requirements.txt ]; then pip install -r Backend/requirements.txt; fi
25
+
26
+ - name: Run tests
27
+ run: |
28
+ python -m pytest api/ -v --ignore=api/test_supabase_auth.py
29
+
30
+ docker-build:
31
+ runs-on: ubuntu-latest
32
+ needs: [backend-tests]
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+
36
+ - name: Set up QEMU
37
+ uses: docker/setup-qemu-action@v2
38
+
39
+ - name: Set up Docker Buildx
40
+ uses: docker/setup-buildx-action@v3
41
+
42
+ - name: Build backend image
43
+ uses: docker/build-push-action@v4
44
+ with:
45
+ context: .
46
+ file: Backend.Dockerfile
47
+ push: false
48
+ tags: backend:local
.gitignore ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===============================
2
+ # Python
3
+ # ===============================
4
+ __pycache__/
5
+ *.py[cod]
6
+ *.pyo
7
+ *.pyd
8
+ *.so
9
+ *.egg-info/
10
+ .eggs/
11
+ *.egg
12
+
13
+ # ===============================
14
+ # Virtual Environments
15
+ # ===============================
16
+ venv/
17
+ .venv/
18
+ env/
19
+ ENV/
20
+ .python-version
21
+ Backend/.env
22
+
23
+ # ===============================
24
+ # Environment Variables
25
+ # ===============================
26
+ .env
27
+ .env.*
28
+ !.env.example
29
+ !.env.*.example
30
+
31
+ # ===============================
32
+ # FastAPI / ASGI
33
+ # ===============================
34
+ *.log
35
+ logs/
36
+ uvicorn.log
37
+ gunicorn.log
38
+
39
+ # ===============================
40
+ # Databases
41
+ # ===============================
42
+ *.sqlite3
43
+ *.db
44
+
45
+ # ===============================
46
+ # Testing / Coverage
47
+ # ===============================
48
+ .pytest_cache/
49
+ .coverage
50
+ htmlcov/
51
+ .tox/
52
+ .nox/
53
+
54
+ # ===============================
55
+ # Build / Distribution
56
+ # ===============================
57
+ build/
58
+ dist/
59
+ wheelhouse/
60
+ pip-wheel-metadata/
61
+
62
+ # ===============================
63
+ # IDEs & Editors
64
+ # ===============================
65
+ .vscode/
66
+ .idea/
67
+ *.swp
68
+ *.swo
69
+
70
+ # ===============================
71
+ # OS Files
72
+ # ===============================
73
+ .DS_Store
74
+ Thumbs.db
75
+
76
+ # ===============================
77
+ # Jupyter
78
+ # ===============================
79
+ .ipynb_checkpoints/
80
+
81
+ # ===============================
82
+ # MyPy / Ruff / Pyright
83
+ # ===============================
84
+ .mypy_cache/
85
+ .ruff_cache/
86
+ .pyright/
87
+
88
+ # ===============================
89
+ # Docker
90
+ # ===============================
91
+ docker-compose.override.yml
92
+
93
+
94
+ **/.env
95
+ **/.env.*
96
+ !**/.env.example
97
+ !**/.env.*.example
98
+
99
+ # ===============================
100
+ # Data & Vector DBs
101
+ # ===============================
102
+ **/vector_db/
103
+ **/chunks/
Backend.Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.12-slim as the base image
2
+ FROM python:3.12-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ build-essential \
10
+ libmagic1 \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy requirements.txt
14
+ COPY requirements.txt .
15
+
16
+ # Install Python dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy the rest of the application code
20
+ COPY . .
21
+
22
+ # Expose the port the app runs on (Hugging Face Spaces uses 7860 by default)
23
+ EXPOSE 7860
24
+
25
+ # Script to build vector DBs and start the server
26
+ RUN echo '#!/bin/bash\n\
27
+ echo "Building Vector Databases..."\n\
28
+ python -m module_a.process_documents\n\
29
+ python -m module_a.build_vector_db\n\
30
+ python -m module_c.indexer\n\
31
+ echo "Starting FastAPI server on port ${PORT:-7860}..."\n\
32
+ uvicorn api.main:app --host 0.0.0.0 --port ${PORT:-7860}\n\
33
+ ' > /app/start.sh && chmod +x /app/start.sh
34
+
35
+ # Run the startup script
36
+ CMD ["/app/start.sh"]
Backend/.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ MONGODB_URL == YOUR_MONGODB_URL
Backend/core/config.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings , SettingsConfigDict
2
+ from functools import lru_cache
3
+
4
+ class Settings(BaseSettings):
5
+
6
+ #MongoDB
7
+ mongodb_url: str
8
+ database_name: str
9
+ secret_key: str
10
+ access_token_expire_minutes: int
11
+ algorithm: str
12
+ frontend_url: str
13
+
14
+ ## To get all the values
15
+ model_config = SettingsConfigDict(env_file=".env")
16
+
17
+ settings = Settings()
Backend/core/deps.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Depends, HTTPException, status
2
+ from fastapi.security import OAuth2PasswordBearer
3
+ from jose import JWTError, jwt
4
+ from db.mongodb import get_database
5
+ from core.config import settings
6
+
7
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
8
+
9
+ async def get_current_user(
10
+ token: str = Depends(oauth2_scheme),
11
+ db = Depends(get_database)
12
+ ):
13
+ credentials_exception = HTTPException(
14
+ status_code=status.HTTP_401_UNAUTHORIZED,
15
+ detail="Could not validate credentials",
16
+ headers={"WWW-Authenticate": "Bearer"},
17
+ )
18
+
19
+ try:
20
+ # Decode the JWT token
21
+ payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
22
+ email: str = payload.get("sub")
23
+ if email is None:
24
+ raise credentials_exception
25
+ except JWTError:
26
+ raise credentials_exception
27
+
28
+ # Find user in database
29
+ user = await db["users"].find_one({"email": email})
30
+ if user is None:
31
+ raise credentials_exception
32
+
33
+ # Convert ObjectId to string and remove password
34
+ user["_id"] = str(user["_id"])
35
+ user.pop("password", None)
36
+
37
+ return user
Backend/core/security.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import bcrypt
2
+ import hashlib
3
+ import base64
4
+ from datetime import datetime, timedelta
5
+ from jose import jwt
6
+ from core.config import settings
7
+
8
+
9
+ def _prehash_password(password: str) -> bytes:
10
+ """Pre-hash password with SHA-256 to handle any length."""
11
+ sha256_hash = hashlib.sha256(password.encode('utf-8')).digest()
12
+ return base64.b64encode(sha256_hash)
13
+
14
+
15
+ def hash_password(password: str) -> str:
16
+ """Hash a password using bcrypt."""
17
+ prehashed = _prehash_password(password)
18
+ salt = bcrypt.gensalt()
19
+ hashed = bcrypt.hashpw(prehashed, salt)
20
+ return hashed.decode('utf-8')
21
+
22
+
23
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
24
+ """Verify a password against its hash."""
25
+ prehashed = _prehash_password(plain_password)
26
+ return bcrypt.checkpw(prehashed, hashed_password.encode('utf-8'))
27
+
28
+
29
+ def create_access_token(data: dict):
30
+ to_encode = data.copy()
31
+ expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
32
+ to_encode.update({"exp": expire})
33
+ return jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
Backend/db/mongodb.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from motor.motor_asyncio import AsyncIOMotorClient
2
+ from core.config import settings
3
+
4
+ class Database:
5
+ client: AsyncIOMotorClient = None
6
+
7
+ db_instance = Database()
8
+
9
+ async def get_database():
10
+ return db_instance.client[settings.database_name]
Backend/main.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI , Request
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from motor.motor_asyncio import AsyncIOMotorClient
4
+ from core.config import settings
5
+ from db.mongodb import db_instance
6
+ from routes import auth
7
+ from contextlib import asynccontextmanager
8
+ from fastapi.exceptions import RequestValidationError
9
+ from fastapi.responses import JSONResponse
10
+
11
+ ## CORS Setup
12
+ @asynccontextmanager
13
+ async def lifespan(app: FastAPI):
14
+ ## Starting logic
15
+ db_instance.client = AsyncIOMotorClient(settings.mongodb_url)
16
+ print("Succesfully Connected to MongoDB")
17
+
18
+ yield
19
+
20
+ db_instance.client.close()
21
+ print("MongoDB connection closed")
22
+
23
+
24
+ app = FastAPI(title="FastAPI Mongo Auth",lifespan=lifespan)
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"],
28
+ allow_credentials= True,
29
+ allow_methods = ["*"],
30
+ allow_headers = ["*"],
31
+ )
32
+ app.include_router(auth.router)
33
+
34
+ @app.exception_handler(RequestValidationError)
35
+ async def validation_exception_handler(request: Request, exc: RequestValidationError):
36
+ print("❌ Validation Error:")
37
+ print(exc.errors()) # This will print in your terminal
38
+ print("📦 Request body:", await request.body())
39
+ return JSONResponse(
40
+ status_code=422,
41
+ content={"detail": exc.errors()}
42
+ )
Backend/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ motor
4
+ pydantic-settings
5
+ passlib[bcrypt]
6
+ python-jose[cryptography]
7
+ python-multipart
Backend/routes/auth.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter , HTTPException , Depends , status
2
+ from schemas.user import UserCreate, Userlogin , Token
3
+ from core.security import hash_password , verify_password , create_access_token
4
+ from db.mongodb import get_database
5
+
6
+ router = APIRouter(prefix="/auth",tags=["Authentication"])
7
+
8
+ @router.post("/register", status_code=status.HTTP_201_CREATED)
9
+ async def signup(user_in: UserCreate, db = Depends(get_database)):
10
+
11
+ # 1. Check if user exists
12
+ user_exists = await db["users"].find_one({'email': user_in.email})
13
+ if user_exists:
14
+ raise HTTPException(status_code=400, detail="User with this email already exists")
15
+
16
+ # 2. Convert Pydantic model to dict
17
+ user_dict = user_in.model_dump()
18
+
19
+ # 3. Replace plain password with hashed password
20
+ user_dict["password"] = hash_password(user_in.password)
21
+
22
+ # 4. Insert into MongoDB
23
+ result = await db["users"].insert_one(user_dict)
24
+
25
+ # 5. Create access token for auto-login
26
+ access_token = create_access_token(data={"sub": user_in.email})
27
+
28
+ # 6. Prepare user data for response (remove password)
29
+ user_response = {k: v for k, v in user_dict.items() if k != "password"}
30
+ user_response["_id"] = str(result.inserted_id)
31
+
32
+ return {
33
+ "message": "User registered successfully",
34
+ "access_token": access_token,
35
+ "token_type": "bearer",
36
+ "user": user_response
37
+ }
38
+
39
+ @router.post("/login",response_model=Token)
40
+ async def login(user_in:Userlogin,db= Depends(get_database)):
41
+ ## Finding the user
42
+ user = await db["users"].find_one({"email":user_in.email})
43
+
44
+ ## Verifying the password
45
+ if not user or not verify_password(user_in.password,user["password"]):
46
+ raise HTTPException(status_code=401,detail="Incorrect email or password")
47
+
48
+ ##Create Token
49
+ access_token = create_access_token(data={"sub":user["email"]})
50
+ return {"access_token":access_token,"token_type": "bearer"}
51
+
52
+ @router.get("/me")
53
+ async def get_user_info(current_user: dict = Depends(get_current_user)):
54
+ """Get current logged-in user's information"""
55
+ return current_user
Backend/schemas/user.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr
2
+ from typing import Optional
3
+
4
+ class UserCreate(BaseModel):
5
+ name: str
6
+ email: EmailStr
7
+ password: str
8
+ nid: str
9
+ age: str
10
+ education: str
11
+ salary: str
12
+ hasDisability: str # or bool if you convert "yes"/"no"
13
+ disabilityType: Optional[str] = None
14
+ province: str
15
+ district: str
16
+ municipality: str
17
+ ward: str
18
+
19
+ class Userlogin(BaseModel):
20
+ email: EmailStr
21
+ password: str
22
+
23
+ class Token(BaseModel):
24
+ access_token: str
25
+ token_type: str
Frontend.Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Node.js 20-alpine as the base image
2
+ FROM node:20-alpine AS base
3
+
4
+ # Install pnpm
5
+ RUN npm install -g pnpm
6
+
7
+ # Set working directory
8
+ WORKDIR /app
9
+
10
+ # Copy package.json and pnpm-lock.yaml
11
+ COPY Frontend/package.json Frontend/pnpm-lock.yaml* ./
12
+
13
+ # Install dependencies
14
+ RUN pnpm install
15
+
16
+ # Copy the rest of the frontend code
17
+ COPY Frontend/ .
18
+
19
+ # Set environment variables for build
20
+ ENV NEXT_PUBLIC_BACKEND_URL=http://localhost:8000
21
+
22
+ # Build the Next.js application
23
+ RUN pnpm build
24
+
25
+ # Expose the port the app runs on
26
+ EXPOSE 3000
27
+
28
+ # Start the application
29
+ CMD ["pnpm", "start"]
Frontend/.env.production.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # --- Frontend Production Environment Variables ---
2
+
3
+ # URL of your deployed FastAPI backend (e.g., Hugging Face Space URL)
4
+ NEXT_PUBLIC_BACKEND_URL=https://your-backend-space.hf.space
Frontend/.gitignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+
6
+ # next.js
7
+ /.next/
8
+ /out/
9
+
10
+ # production
11
+ /build
12
+
13
+ # debug
14
+ npm-debug.log*
15
+ yarn-debug.log*
16
+ yarn-error.log*
17
+ .pnpm-debug.log*
18
+
19
+ # env files
20
+ .env*
21
+ !.env.example
22
+ !.env.*.example
23
+
24
+ # vercel
25
+ .vercel
26
+
27
+ # typescript
28
+ *.tsbuildinfo
29
+ next-env.d.ts
Frontend/app/api/Bias_Chat/route.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type NextRequest, NextResponse } from "next/server"
2
+
3
+ export async function POST(req: NextRequest) {
4
+ try {
5
+ const { messages, type } = await req.json()
6
+
7
+ // Example for a custom AI API call:
8
+ // const response = await fetch("https://your-ai-api.com/v1/chat", {
9
+ // method: "POST",
10
+ // headers: { "Authorization": `Bearer ${process.env.MY_AI_KEY}` },
11
+ // body: JSON.stringify({ messages })
12
+ // })
13
+ // const data = await response.json()
14
+
15
+ // For now, we'll simulate a real API response that you can easily swap
16
+ const lastMessage = messages[messages.length - 1].content
17
+
18
+ let aiContent = ""
19
+ if (type === "legal") {
20
+ aiContent = `Based on your query about "${lastMessage}", under Nepali Law, this is typically governed by specific statutes...`
21
+ } else {
22
+ aiContent = "Standard AI response for non-legal queries."
23
+ }
24
+
25
+ return NextResponse.json({ content: aiContent })
26
+ } catch (error) {
27
+ console.error("[v0] AI API Error:", error)
28
+ return NextResponse.json({ error: "Failed to fetch AI response" }, { status: 500 })
29
+ }
30
+ }
Frontend/app/api/Legal_Chat/route.ts ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type NextRequest, NextResponse } from "next/server"
2
+
3
+ const BACKEND_URL = "http://localhost:8000"
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const { messages, type, conversation_id } = await req.json()
8
+
9
+ if (type !== "legal") {
10
+ return NextResponse.json({ content: "Standard AI response for non-legal queries." })
11
+ }
12
+
13
+ const lastMessage = messages[messages.length - 1].content
14
+
15
+ // Get Authorization header from incoming request
16
+ const authHeader = req.headers.get("authorization")
17
+ const headers: Record<string, string> = { "Content-Type": "application/json" }
18
+ if (authHeader) {
19
+ headers["Authorization"] = authHeader
20
+ }
21
+
22
+ // Use the new context-aware chat endpoint
23
+ const response = await fetch(`${BACKEND_URL}/api/v1/law-explanation/chat`, {
24
+ method: "POST",
25
+ headers,
26
+ body: JSON.stringify({
27
+ query: lastMessage,
28
+ conversation_id: conversation_id || null,
29
+ }),
30
+ })
31
+
32
+ if (!response.ok) {
33
+ throw new Error(`Backend API returned ${response.status}`)
34
+ }
35
+
36
+ const data = await response.json()
37
+
38
+ // Check if this is a non-legal query (greeting, thanks, etc.)
39
+ if (data.is_non_legal) {
40
+ return NextResponse.json({ content: data.explanation })
41
+ }
42
+
43
+ // Format sources properly from backend response
44
+ let sourcesText = ""
45
+ console.log("[Legal Chat] Sources from backend:", data.sources)
46
+
47
+ if (data.sources && data.sources.length > 0) {
48
+ const formattedSources = data.sources
49
+ .filter((s: any) => s && (s.file || s.section)) // Only include sources with file or section
50
+ .map((s: any, i: number) => {
51
+ // Extract and clean filename
52
+ let fileName = "Legal Document"
53
+ if (s.file) {
54
+ fileName = s.file
55
+ .replace(/_en\.pdf$/i, "") // Remove _en.pdf extension
56
+ .replace(/\.pdf$/i, "") // Remove .pdf extension
57
+ .replace(/_/g, " ") // Replace underscores with spaces
58
+ }
59
+
60
+ // Format: **filename** (section)
61
+ const section = s.section || s.article_section || "General Reference"
62
+ return ` ${i + 1}. **${fileName}** (${section})`
63
+ })
64
+ .join("\n")
65
+
66
+ if (formattedSources) {
67
+ sourcesText = `\n\n### 📚 Resources\n${formattedSources}`
68
+ }
69
+ }
70
+
71
+ // Add context indicator if context was used
72
+ const contextBadge = data.context_used ? "\n\n> 💡 *Used conversation context*" : ""
73
+
74
+ // Return the data as-is since backend already returns markdown format
75
+ // Format it nicely for the frontend with clear sections
76
+ const formattedContent = `### 📝 Summary\n${data.summary}
77
+
78
+ ### 💬 Detailed Explanation\n${data.explanation}
79
+
80
+ ### 🔑 Key Points
81
+ - ${data.key_point}
82
+
83
+ ### 📋 Next Steps
84
+ ${data.next_steps}${sourcesText}${contextBadge}`.trim()
85
+
86
+ return NextResponse.json({
87
+ content: formattedContent,
88
+ suggested_action: data.suggested_action || null
89
+ })
90
+ } catch (error) {
91
+ console.error("[Legal Chat API Error]:", error)
92
+ return NextResponse.json(
93
+ { error: "Failed to fetch response from legal AI backend. Please ensure the backend server is running." },
94
+ { status: 500 }
95
+ )
96
+ }
97
+ }
Frontend/app/api/bias-detection-hitl/approve/route.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type NextRequest, NextResponse } from "next/server"
2
+
3
+ const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000"
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const authHeader = req.headers.get("authorization")
8
+ const headers: Record<string, string> = {
9
+ "Content-Type": "application/json"
10
+ }
11
+ if (authHeader) {
12
+ headers["Authorization"] = authHeader
13
+ }
14
+
15
+ const body = await req.json()
16
+
17
+ const response = await fetch(`${BACKEND_URL}/api/v1/bias-detection-hitl/approve-suggestion`, {
18
+ method: "POST",
19
+ headers,
20
+ body: JSON.stringify(body),
21
+ })
22
+
23
+ if (!response.ok) {
24
+ const errorData = await response.json().catch(() => ({}))
25
+ throw new Error(errorData.detail || "Failed to approve/reject suggestion")
26
+ }
27
+
28
+ const data = await response.json()
29
+ return NextResponse.json(data)
30
+ } catch (error) {
31
+ console.error("[HITL Approve Error]:", error)
32
+ return NextResponse.json(
33
+ {
34
+ error: error instanceof Error ? error.message : "Failed to process approval",
35
+ },
36
+ { status: 500 }
37
+ )
38
+ }
39
+ }
Frontend/app/api/bias-detection-hitl/generate-pdf/route.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type NextRequest, NextResponse } from "next/server"
2
+
3
+ const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000"
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const authHeader = req.headers.get("authorization")
8
+ const headers: Record<string, string> = {
9
+ "Content-Type": "application/json"
10
+ }
11
+ if (authHeader) {
12
+ headers["Authorization"] = authHeader
13
+ }
14
+
15
+ const body = await req.json()
16
+
17
+ const response = await fetch(`${BACKEND_URL}/api/v1/bias-detection-hitl/generate-pdf`, {
18
+ method: "POST",
19
+ headers,
20
+ body: JSON.stringify(body),
21
+ })
22
+
23
+ if (!response.ok) {
24
+ const errorData = await response.json().catch(() => ({}))
25
+ throw new Error(errorData.detail || "Failed to generate PDF")
26
+ }
27
+
28
+ // Get PDF blob
29
+ const pdfBlob = await response.blob()
30
+
31
+ // Get filename from headers
32
+ const contentDisposition = response.headers.get("Content-Disposition")
33
+ const changesApplied = response.headers.get("X-Changes-Applied")
34
+
35
+ // Return PDF as blob with headers
36
+ return new NextResponse(pdfBlob, {
37
+ status: 200,
38
+ headers: {
39
+ "Content-Type": "application/pdf",
40
+ "Content-Disposition": contentDisposition || 'attachment; filename="debiased_document.pdf"',
41
+ "X-Changes-Applied": changesApplied || "0"
42
+ }
43
+ })
44
+ } catch (error) {
45
+ console.error("[HITL Generate PDF Error]:", error)
46
+ return NextResponse.json(
47
+ {
48
+ error: error instanceof Error ? error.message : "Failed to generate PDF",
49
+ },
50
+ { status: 500 }
51
+ )
52
+ }
53
+ }
Frontend/app/api/bias-detection-hitl/regenerate/route.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type NextRequest, NextResponse } from "next/server"
2
+
3
+ const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000"
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const authHeader = req.headers.get("authorization")
8
+ const headers: Record<string, string> = {
9
+ "Content-Type": "application/json"
10
+ }
11
+ if (authHeader) {
12
+ headers["Authorization"] = authHeader
13
+ }
14
+
15
+ const body = await req.json()
16
+
17
+ const response = await fetch(`${BACKEND_URL}/api/v1/bias-detection-hitl/regenerate-suggestion`, {
18
+ method: "POST",
19
+ headers,
20
+ body: JSON.stringify(body),
21
+ })
22
+
23
+ if (!response.ok) {
24
+ const errorData = await response.json().catch(() => ({}))
25
+ throw new Error(errorData.detail || "Failed to regenerate suggestion")
26
+ }
27
+
28
+ const data = await response.json()
29
+ return NextResponse.json(data)
30
+ } catch (error) {
31
+ console.error("[HITL Regenerate Error]:", error)
32
+ return NextResponse.json(
33
+ {
34
+ error: error instanceof Error ? error.message : "Failed to regenerate suggestion",
35
+ },
36
+ { status: 500 }
37
+ )
38
+ }
39
+ }
Frontend/app/api/bias-detection/route.ts ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type NextRequest, NextResponse } from "next/server"
2
+
3
+ const BACKEND_URL = "http://localhost:8000"
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ // Get Authorization header from incoming request
8
+ const authHeader = req.headers.get("authorization")
9
+ const headers: Record<string, string> = {}
10
+ if (authHeader) {
11
+ headers["Authorization"] = authHeader
12
+ }
13
+
14
+ const formData = await req.formData()
15
+ const file = formData.get("file") as File | null
16
+ const text = formData.get("text") as string | null
17
+ const confidenceThreshold = parseFloat(formData.get("confidence_threshold") as string || "0.7")
18
+ const useHITL = formData.get("use_hitl") === "true"
19
+
20
+ // If using HITL workflow for PDF files
21
+ if (file && useHITL) {
22
+ console.log("Using HITL workflow for PDF processing...")
23
+ const hitlFormData = new FormData()
24
+ hitlFormData.append("file", file)
25
+ hitlFormData.append("refine_with_llm", "true")
26
+ hitlFormData.append("confidence_threshold", confidenceThreshold.toString())
27
+
28
+ const response = await fetch(`${BACKEND_URL}/api/v1/bias-detection-hitl/start-review`, {
29
+ method: "POST",
30
+ headers,
31
+ body: hitlFormData,
32
+ })
33
+
34
+ if (!response.ok) {
35
+ const errorData = await response.json().catch(() => ({}))
36
+ throw new Error(errorData.detail || "Failed to start HITL review")
37
+ }
38
+
39
+ const data = await response.json()
40
+ console.log("HITL session created:", data.session_id)
41
+
42
+ // Return the HITL session data
43
+ return NextResponse.json(data)
44
+ }
45
+
46
+ // Original non-HITL workflow for text or non-HITL PDF processing
47
+ let sentences: string[] = []
48
+ let filename: string | undefined
49
+
50
+ if (file) {
51
+ // Step 1: Process PDF to extract sentences with LLM refinement
52
+ console.log("Step 1: Processing PDF to extract sentences...")
53
+ const pdfFormData = new FormData()
54
+ pdfFormData.append("file", file)
55
+ pdfFormData.append("refine_with_llm", "true")
56
+
57
+ const pdfResponse = await fetch(`${BACKEND_URL}/api/v1/process-pdf`, {
58
+ method: "POST",
59
+ headers,
60
+ body: pdfFormData,
61
+ })
62
+
63
+ if (!pdfResponse.ok) {
64
+ throw new Error("Failed to process PDF")
65
+ }
66
+
67
+ const pdfData = await pdfResponse.json()
68
+ sentences = pdfData.sentences
69
+ filename = pdfData.filename
70
+ console.log(`Extracted ${sentences.length} sentences from PDF`)
71
+ } else if (text) {
72
+ // For text input, use single sentence as array
73
+ sentences = [text]
74
+ } else {
75
+ return NextResponse.json({ error: "Either file or text must be provided" }, { status: 400 })
76
+ }
77
+
78
+ // Step 2: Detect bias using batch endpoint
79
+ console.log("Step 2: Detecting bias in sentences...")
80
+ const biasHeaders = { ...headers, "Content-Type": "application/json" }
81
+ const batchBiasResponse = await fetch(`${BACKEND_URL}/api/v1/detect-bias/batch`, {
82
+ method: "POST",
83
+ headers: biasHeaders,
84
+ body: JSON.stringify({
85
+ texts: sentences,
86
+ confidence_threshold: confidenceThreshold,
87
+ }),
88
+ })
89
+
90
+ if (!batchBiasResponse.ok) {
91
+ throw new Error("Failed to detect bias")
92
+ }
93
+
94
+ const batchBiasData = await batchBiasResponse.json()
95
+
96
+ // Count biased and unbiased sentences
97
+ let biasedCount = 0
98
+ let unbiasedCount = 0
99
+ const biasedSentencesData: Array<{
100
+ sentence: string
101
+ category: string
102
+ confidence: number
103
+ explanation?: string
104
+ }> = []
105
+
106
+ // Process batch results
107
+ for (const item of batchBiasData.items) {
108
+ const result = item.result
109
+ if (result.success && result.results) {
110
+ for (const sentenceResult of result.results) {
111
+ if (sentenceResult.is_biased) {
112
+ biasedCount++
113
+ biasedSentencesData.push({
114
+ sentence: sentenceResult.sentence,
115
+ category: sentenceResult.category,
116
+ confidence: sentenceResult.confidence,
117
+ })
118
+ } else {
119
+ unbiasedCount++
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ console.log(`Found ${biasedCount} biased sentences and ${unbiasedCount} unbiased sentences`)
126
+
127
+ // Step 3: Get debiasing suggestions for biased sentences
128
+ console.log("Step 3: Getting debiasing suggestions...")
129
+ const suggestionsData: Array<{
130
+ original: string
131
+ category: string
132
+ confidence: number
133
+ suggestion: string | null
134
+ explanation: string | null
135
+ success: boolean
136
+ }> = []
137
+
138
+ if (biasedSentencesData.length > 0) {
139
+ // Prepare batch debias request
140
+ const debiasItems = biasedSentencesData.map((item) => ({
141
+ sentence: item.sentence,
142
+ category: item.category,
143
+ context: null,
144
+ }))
145
+
146
+ const debiasResponse = await fetch(`${BACKEND_URL}/api/v1/debias-sentence/batch`, {
147
+ method: "POST",
148
+ headers: biasHeaders,
149
+ body: JSON.stringify({
150
+ items: debiasItems,
151
+ }),
152
+ })
153
+
154
+ if (debiasResponse.ok) {
155
+ const debiasData = await debiasResponse.json()
156
+
157
+ for (let i = 0; i < biasedSentencesData.length; i++) {
158
+ const original = biasedSentencesData[i]
159
+ const debiasResult = debiasData.items?.[i]?.result
160
+
161
+ // Generate explanation based on category
162
+ const categoryExplanations: Record<string, string> = {
163
+ gender: "This sentence contains gender-based bias, using language that may stereotype, exclude, or unfairly characterize individuals based on their gender.",
164
+ religional: "This sentence contains religious bias, using language that may discriminate against or unfairly characterize individuals based on their religious beliefs.",
165
+ caste: "This sentence contains caste-based bias, using language that may perpetuate discrimination or unfair treatment based on caste identity.",
166
+ religion: "This sentence contains religious bias, with language that may show prejudice or discrimination based on religious affiliation.",
167
+ appearence: "This sentence contains appearance-based bias, using language that may judge or discriminate based on physical appearance or looks.",
168
+ socialstatus: "This sentence contains social status bias, using language that may discriminate or show prejudice based on socioeconomic status or class.",
169
+ political: "This sentence contains political bias, showing unfair preference or prejudice toward a particular political viewpoint or party.",
170
+ Age: "This sentence contains age-based bias, using language that may stereotype or discriminate based on age or generation.",
171
+ Disablity: "This sentence contains disability-based bias, using language that may discriminate against or unfairly characterize individuals with disabilities.",
172
+ amiguity: "This sentence contains ambiguous language that may lead to misinterpretation or unclear bias.",
173
+ }
174
+
175
+ const explanation = categoryExplanations[original.category] || "This sentence has been flagged for potential bias."
176
+
177
+ suggestionsData.push({
178
+ original: original.sentence,
179
+ category: original.category,
180
+ confidence: original.confidence,
181
+ suggestion: debiasResult?.suggestion || null,
182
+ explanation: explanation,
183
+ success: debiasResult?.success || false,
184
+ })
185
+ }
186
+ } else {
187
+ // If debias fails, still return results without suggestions
188
+ console.warn("Debiasing failed, returning results without suggestions")
189
+ for (const item of biasedSentencesData) {
190
+ suggestionsData.push({
191
+ original: item.sentence,
192
+ category: item.category,
193
+ confidence: item.confidence,
194
+ suggestion: null,
195
+ explanation: null,
196
+ success: false,
197
+ })
198
+ }
199
+ }
200
+ }
201
+
202
+ console.log("Analysis complete!")
203
+
204
+ // Return formatted response
205
+ return NextResponse.json({
206
+ success: true,
207
+ biasedCount,
208
+ unbiasedCount,
209
+ totalSentences: biasedCount + unbiasedCount,
210
+ biasedSentences: suggestionsData,
211
+ filename,
212
+ })
213
+ } catch (error) {
214
+ console.error("[Bias Detection API Error]:", error)
215
+ return NextResponse.json(
216
+ {
217
+ error: error instanceof Error ? error.message : "Failed to analyze bias. Please ensure the backend server is running.",
218
+ },
219
+ { status: 500 }
220
+ )
221
+ }
222
+ }
Frontend/app/api/letter-generation/route.ts ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type NextRequest, NextResponse } from "next/server"
2
+
3
+ const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000"
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const body = await req.json()
8
+ const { action, data } = body
9
+
10
+ // Get Authorization header from incoming request
11
+ const authHeader = req.headers.get("authorization")
12
+ const headers: Record<string, string> = { "Content-Type": "application/json" }
13
+ if (authHeader) {
14
+ headers["Authorization"] = authHeader
15
+ }
16
+
17
+ let response: Response
18
+ let result: any
19
+
20
+ switch (action) {
21
+ case "search-template":
22
+ // Step 1: Search for template based on user requirement
23
+ response = await fetch(`${BACKEND_URL}/api/v1/search-template`, {
24
+ method: "POST",
25
+ headers,
26
+ body: JSON.stringify({ query: data.query }),
27
+ })
28
+
29
+ if (!response.ok) {
30
+ throw new Error("Failed to search template")
31
+ }
32
+
33
+ result = await response.json()
34
+ return NextResponse.json({
35
+ success: result.success,
36
+ templateName: result.template_name,
37
+ score: result.score,
38
+ content: result.content,
39
+ error: result.error,
40
+ })
41
+
42
+ case "get-template-details":
43
+ // Step 2: Get placeholder details for the template
44
+ response = await fetch(`${BACKEND_URL}/api/v1/get-template-details`, {
45
+ method: "POST",
46
+ headers,
47
+ body: JSON.stringify({ template_name: data.templateName }),
48
+ })
49
+
50
+ if (!response.ok) {
51
+ throw new Error("Failed to get template details")
52
+ }
53
+
54
+ result = await response.json()
55
+ return NextResponse.json({
56
+ success: result.success,
57
+ templateName: result.template_name,
58
+ placeholders: result.placeholders,
59
+ content: result.content,
60
+ error: result.error,
61
+ })
62
+
63
+ case "fill-template":
64
+ // Step 3: Fill template with provided placeholder values
65
+ response = await fetch(`${BACKEND_URL}/api/v1/fill-template`, {
66
+ method: "POST",
67
+ headers,
68
+ body: JSON.stringify({
69
+ template_name: data.templateName,
70
+ placeholders: data.placeholders,
71
+ }),
72
+ })
73
+
74
+ if (!response.ok) {
75
+ throw new Error("Failed to fill template")
76
+ }
77
+
78
+ result = await response.json()
79
+ return NextResponse.json({
80
+ success: result.success,
81
+ letter: result.letter,
82
+ error: result.error,
83
+ })
84
+
85
+ default:
86
+ return NextResponse.json({ error: "Invalid action" }, { status: 400 })
87
+ }
88
+ } catch (error) {
89
+ console.error("[Letter Generation API Error]:", error)
90
+ return NextResponse.json(
91
+ {
92
+ error: error instanceof Error ? error.message : "Failed to process request. Please ensure the backend server is running.",
93
+ },
94
+ { status: 500 }
95
+ )
96
+ }
97
+ }
Frontend/app/api/login/route.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type NextRequest, NextResponse } from "next/server"
2
+
3
+ export async function POST(req: NextRequest) {
4
+ try {
5
+ const body = await req.json()
6
+
7
+ const backendRes = await fetch("http://localhost:8000/login", {
8
+ method: "POST",
9
+ headers: { "Content-Type": "application/json" },
10
+ body: JSON.stringify(body),
11
+ })
12
+
13
+ const data = await backendRes.json().catch(() => null)
14
+
15
+ return NextResponse.json(data ?? { message: "No JSON response from backend" }, { status: backendRes.status })
16
+ } catch (error) {
17
+ console.error("/api/login proxy error:", error)
18
+ return NextResponse.json({ error: "Proxy failed" }, { status: 500 })
19
+ }
20
+ }
Frontend/app/bias-checker/page.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { BiasChecker } from "@/components/chatbot/bias-checker"
4
+ import { Navbar } from "@/components/layout/navbar"
5
+ import { Footer } from "@/components/layout/footer"
6
+ import { useAuthGuard } from "@/hooks/use-auth-guard"
7
+
8
+ export default function BiasCheckerPage() {
9
+ const { isLoading, isAuthenticated } = useAuthGuard()
10
+
11
+ if (isLoading) {
12
+ return (
13
+ <div className="flex flex-col min-h-screen">
14
+ <Navbar />
15
+ <main className="flex-1 flex items-center justify-center">
16
+ <div className="text-center">
17
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
18
+ <p className="text-muted-foreground">Checking authentication...</p>
19
+ </div>
20
+ </main>
21
+ <Footer />
22
+ </div>
23
+ )
24
+ }
25
+
26
+ if (!isAuthenticated) {
27
+ return null // Will redirect to login
28
+ }
29
+
30
+ return (
31
+ <div className="flex flex-col min-h-screen">
32
+ <Navbar />
33
+ <main className="flex-1 p-4 md:p-8 bg-muted/20">
34
+ <div className="container mx-auto max-w-7xl">
35
+ <div className="mb-8">
36
+ <h1 className="text-3xl font-bold text-primary tracking-tight">Legal Notice Analyzer</h1>
37
+ <p className="text-muted-foreground">
38
+ Empowering fair communication by detecting socioeconomic and linguistic biases in legal documents.
39
+ </p>
40
+ </div>
41
+ <BiasChecker />
42
+ </div>
43
+ </main>
44
+ <Footer />
45
+ </div>
46
+ )
47
+ }
Frontend/app/chatbot/page.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { LawChatbot } from "@/components/chatbot/law-chatbot"
4
+ import { Navbar } from "@/components/layout/navbar"
5
+ import { Footer } from "@/components/layout/footer"
6
+ import { useAuthGuard } from "@/hooks/use-auth-guard"
7
+
8
+ export default function ChatbotPage() {
9
+ const { isLoading, isAuthenticated } = useAuthGuard()
10
+
11
+ if (isLoading) {
12
+ return (
13
+ <div className="flex flex-col min-h-screen">
14
+ <Navbar />
15
+ <main className="flex-1 flex items-center justify-center">
16
+ <div className="text-center">
17
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
18
+ <p className="text-muted-foreground">Checking authentication...</p>
19
+ </div>
20
+ </main>
21
+ <Footer />
22
+ </div>
23
+ )
24
+ }
25
+
26
+ if (!isAuthenticated) {
27
+ return null // Will redirect to login
28
+ }
29
+
30
+ return (
31
+ <div className="flex flex-col min-h-screen">
32
+ <Navbar />
33
+ <main className="flex-1 p-4 md:p-8 bg-muted/20">
34
+ <div className="container mx-auto max-w-7xl h-full">
35
+ <div className="mb-6">
36
+ <h1 className="text-2xl font-bold text-primary">Nepali Law Assistant</h1>
37
+ <p className="text-sm text-muted-foreground">Ask questions about Nepali law, rights, and regulations.</p>
38
+ </div>
39
+ <LawChatbot />
40
+ </div>
41
+ </main>
42
+ <Footer />
43
+ </div>
44
+ )
45
+ }
Frontend/app/dashboard/page.tsx ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ import { useAuth } from "@/context/auth-context"
7
+ import { Navbar } from "@/components/layout/navbar"
8
+ import { Footer } from "@/components/layout/footer"
9
+ import { Button } from "@/components/ui/button"
10
+ import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card"
11
+ import { StatsCard } from "@/components/dashboard/stats-card"
12
+ import { MessageSquare, ShieldCheck, Clock, ArrowUpRight, BookOpen, Lightbulb, Bell, FileText } from "lucide-react"
13
+ import Link from "next/link"
14
+ import { redirect } from "next/navigation"
15
+ import { getDocumentStats } from "@/lib/document-cache"
16
+
17
+ export default function DashboardPage() {
18
+ const { user, isLoading } = useAuth()
19
+ const [mounted, setMounted] = useState(false)
20
+
21
+ useEffect(() => {
22
+ setMounted(true)
23
+ console.log("Dashboard - isLoading:", isLoading);
24
+ console.log("Dashboard - user:", user);
25
+ console.log("Token in localStorage:", localStorage.getItem('access_token'));
26
+ }, [isLoading, user])
27
+
28
+ const [tips, setTips] = useState<any[]>([])
29
+ const [currentTipIndex, setCurrentTipIndex] = useState<number>(0)
30
+ const [showNepali, setShowNepali] = useState<boolean>(false)
31
+ const [totalConsultations, setTotalConsultations] = useState<number>(0)
32
+ const [recentActivities, setRecentActivities] = useState<any[]>([])
33
+ const [documentStats, setDocumentStats] = useState({
34
+ totalAnalyzed: 0,
35
+ totalInclusive: 0,
36
+ totalFlagged: 0,
37
+ totalLetters: 0,
38
+ })
39
+
40
+ useEffect(() => {
41
+ fetch("/data.json")
42
+ .then((res) => res.json())
43
+ .then((data) => {
44
+ if (Array.isArray(data) && data.length > 0) {
45
+ setTips(data)
46
+ setCurrentTipIndex(Math.floor(Math.random() * data.length))
47
+ }
48
+ })
49
+ .catch((err) => console.error("Failed to load tips:", err))
50
+
51
+ // Load document stats from cache
52
+ const stats = getDocumentStats()
53
+ setDocumentStats(stats)
54
+ }, [])
55
+
56
+ // Fetch total consultations count and recent activities
57
+ useEffect(() => {
58
+ const fetchConsultations = async () => {
59
+ const token = localStorage.getItem("access_token")
60
+ if (!token) return
61
+
62
+ const BACKEND_URL = (process.env.NEXT_PUBLIC_BACKEND_URL as string) || "http://localhost:8000"
63
+
64
+ try {
65
+ const response = await fetch(`${BACKEND_URL}/api/v1/chat-history/conversations`, {
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ Authorization: `Bearer ${token}`,
69
+ },
70
+ })
71
+
72
+ if (response.ok) {
73
+ const data = await response.json()
74
+ setTotalConsultations(data.length)
75
+
76
+ // Transform conversations into activity format
77
+ const activities = data.slice(0, 5).map((conv: any) => ({
78
+ type: "chat",
79
+ title: conv.title,
80
+ time: formatRelativeTime(conv.updated_at),
81
+ icon: MessageSquare,
82
+ color: "text-primary bg-primary/10",
83
+ conversationId: conv.id,
84
+ }))
85
+ setRecentActivities(activities)
86
+ }
87
+ } catch (error) {
88
+ console.error("Failed to fetch consultations:", error)
89
+ }
90
+ }
91
+
92
+ if (user) {
93
+ fetchConsultations()
94
+ }
95
+ }, [user])
96
+
97
+ // Helper function to format relative time
98
+ const formatRelativeTime = (dateString: string) => {
99
+ const date = new Date(dateString)
100
+ const now = new Date()
101
+ const diffMs = now.getTime() - date.getTime()
102
+ const diffMins = Math.floor(diffMs / 60000)
103
+ const diffHours = Math.floor(diffMs / 3600000)
104
+ const diffDays = Math.floor(diffMs / 86400000)
105
+
106
+ if (diffMins < 60) {
107
+ return diffMins <= 1 ? "Just now" : `${diffMins} minutes ago`
108
+ } else if (diffHours < 24) {
109
+ return diffHours === 1 ? "1 hour ago" : `${diffHours} hours ago`
110
+ } else if (diffDays === 1) {
111
+ return "Yesterday"
112
+ } else if (diffDays < 7) {
113
+ return `${diffDays} days ago`
114
+ } else {
115
+ return date.toLocaleDateString()
116
+ }
117
+ }
118
+
119
+ if (!mounted || isLoading) return null
120
+ if (!user) redirect("/login")
121
+
122
+ // Calculate profile progress based on name, email, NID, and age
123
+ const calculateProfileProgress = () => {
124
+ let completedFields = 0
125
+ const totalFields = 4
126
+
127
+ // Name is always present (required during registration)
128
+ if (user.name) completedFields++
129
+
130
+ // Email is always present (required during registration)
131
+ if (user.email) completedFields++
132
+
133
+ // Check if NID is filled
134
+ if (user.details?.nid) completedFields++
135
+
136
+ // Check if age is filled
137
+ if (user.details?.age) completedFields++
138
+
139
+ return Math.round((completedFields / totalFields) * 100)
140
+ }
141
+
142
+ const profileProgress = calculateProfileProgress()
143
+
144
+
145
+ return (
146
+ <div className="flex flex-col min-h-screen">
147
+ <Navbar />
148
+ <main className="flex-1 p-4 md:p-8 bg-muted/20">
149
+ <div className="container mx-auto max-w-7xl">
150
+ {/* Header */}
151
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
152
+ <div className="space-y-1">
153
+ <h1 className="text-3xl font-bold tracking-tight">Welcome back, {user.name}</h1>
154
+ <p className="text-muted-foreground">Here&apos;s an overview of your legal welfare activity.</p>
155
+ </div>
156
+ <div className="flex items-center gap-3">
157
+ <Button variant="outline" size="icon" className="relative bg-transparent">
158
+ <Bell className="h-5 w-5" />
159
+ <span className="absolute top-1 right-1 h-2 w-2 rounded-full bg-destructive" />
160
+ </Button>
161
+ <Button className="bg-primary" asChild>
162
+ <Link href="/chatbot">Start New Consultation</Link>
163
+ </Button>
164
+ </div>
165
+ </div>
166
+
167
+ {/* Stats Grid */}
168
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
169
+ <StatsCard
170
+ title="Total Consultations"
171
+ value={totalConsultations.toString()}
172
+ description={`${totalConsultations} chat ${totalConsultations === 1 ? 'history' : 'histories'}`}
173
+ icon={MessageSquare}
174
+ color="primary"
175
+ />
176
+ <StatsCard
177
+ title="Documents Analyzed"
178
+ value={documentStats.totalAnalyzed.toString()}
179
+ description={`${documentStats.totalInclusive} inclusive, ${documentStats.totalFlagged} flagged`}
180
+ icon={ShieldCheck}
181
+ color="accent"
182
+ />
183
+ <StatsCard
184
+ title="Letters Generated"
185
+ value={documentStats.totalLetters.toString()}
186
+ description="Cached locally"
187
+ icon={FileText}
188
+ color="success"
189
+ />
190
+ <Link href="/profile" className="block">
191
+ <StatsCard
192
+ title="Profile Progress"
193
+ value={`${profileProgress}%`}
194
+ description="Complete your details"
195
+ icon={ArrowUpRight}
196
+ color="primary"
197
+ />
198
+ </Link>
199
+ </div>
200
+
201
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
202
+ {/* Recent Activity */}
203
+ <Card className="lg:col-span-2 border-primary/10">
204
+ <CardHeader className="flex flex-row items-center justify-between">
205
+ <div>
206
+ <CardTitle>Recent Activity</CardTitle>
207
+ <CardDescription>Your latest interactions with our legal tools.</CardDescription>
208
+ </div>
209
+ <Button variant="ghost" size="sm" className="text-primary">
210
+ View All
211
+ </Button>
212
+ </CardHeader>
213
+ <CardContent>
214
+ <div className="space-y-6">
215
+ {recentActivities.length > 0 ? (
216
+ recentActivities.map((activity, i) => (
217
+ <Link key={i} href="/chatbot">
218
+ <div className="flex items-start gap-4 group cursor-pointer">
219
+ <div className={cn("p-2 rounded-lg border border-transparent transition-colors", activity.color)}>
220
+ <activity.icon className="h-5 w-5" />
221
+ </div>
222
+ <div className="flex-1 space-y-1">
223
+ <p className="text-sm font-bold group-hover:text-primary transition-colors">{activity.title}</p>
224
+ <p className="text-xs text-muted-foreground">{activity.time}</p>
225
+ </div>
226
+ <ArrowUpRight className="h-4 w-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
227
+ </div>
228
+ </Link>
229
+ ))
230
+ ) : (
231
+ <div className="text-center py-8 text-muted-foreground">
232
+ <MessageSquare className="h-12 w-12 mx-auto mb-3 opacity-20" />
233
+ <p className="text-sm">No recent activity yet</p>
234
+ <p className="text-xs mt-1">Start a conversation with our legal chatbot</p>
235
+ </div>
236
+ )}
237
+ </div>
238
+ </CardContent>
239
+ </Card>
240
+
241
+ {/* Side Panel: Tips & Actions */}
242
+ <div className="space-y-8">
243
+ <Card className="bg-primary text-primary-foreground border-none relative overflow-hidden">
244
+ <div className="absolute inset-0 nepali-pattern opacity-10" />
245
+ <CardHeader className="relative">
246
+ <CardTitle className="flex items-center gap-2">
247
+ <Lightbulb className="h-5 w-5 text-accent" />
248
+ Legal Tip of the Day
249
+ </CardTitle>
250
+ </CardHeader>
251
+ <CardContent className="relative">
252
+ <p className="text-sm leading-relaxed opacity-90">
253
+ {tips.length > 0 ? (showNepali ? tips[currentTipIndex]?.fact_np : tips[currentTipIndex]?.fact_en) : "Loading tip..."}
254
+ </p>
255
+
256
+ <div className="mt-4 w-full flex gap-3 items-center">
257
+ <Button
258
+ variant="ghost"
259
+ size="lg"
260
+ className="flex-1 bg-white/10 hover:bg-white/20 text-white/95 rounded-lg px-4 py-2 flex items-center gap-3 justify-center shadow-md"
261
+ onClick={() => {
262
+ // pick a new random tip (keep current language)
263
+ if (tips.length > 1) {
264
+ let idx = Math.floor(Math.random() * tips.length)
265
+ if (tips.length > 1 && idx === currentTipIndex) idx = (idx + 1) % tips.length
266
+ setCurrentTipIndex(idx)
267
+ }
268
+ }}
269
+ title={showNepali ? "नेपालीमा" : "View more tips"}
270
+ >
271
+ <Lightbulb className="h-4 w-4 text-accent" />
272
+ <span className="font-medium">
273
+ {showNepali ? "थप सुझावहरू" : "View More Tips"}
274
+ </span>
275
+ </Button>
276
+
277
+ <Button
278
+ variant="default"
279
+ size="sm"
280
+ className="rounded-full w-12 h-12 flex items-center justify-center shadow ring-1 ring-white/10 hover:scale-105 transition-transform"
281
+ onClick={() => setShowNepali((s) => !s)}
282
+ aria-label={showNepali ? "Switch to English" : "नेपालीमा हेर्नुहोस्"}
283
+ title={showNepali ? "Show in English" : "नेपालीमा हेर्नुहोस्"}
284
+ >
285
+ <span className="text-lg">{showNepali ? "EN" : "🇳🇵"}</span>
286
+ </Button>
287
+ </div>
288
+ </CardContent>
289
+ </Card>
290
+
291
+ <Card className="border-primary/10">
292
+ <CardHeader>
293
+ <CardTitle>Quick Actions</CardTitle>
294
+ </CardHeader>
295
+ <CardContent className="grid gap-4">
296
+ <Button variant="outline" className="w-full justify-start text-sm bg-transparent" asChild>
297
+ <Link href="/bias-checker">
298
+ <ShieldCheck className="mr-2 h-4 w-4 text-accent" />
299
+ Analyze a New Document
300
+ </Link>
301
+ </Button>
302
+ <Button variant="outline" className="w-full justify-start text-sm bg-transparent" asChild>
303
+ <Link href="/resources">
304
+ <BookOpen className="mr-2 h-4 w-4 text-success" />
305
+ Browse Legal Guides
306
+ </Link>
307
+ </Button>
308
+ <Button variant="outline" className="w-full justify-start text-sm bg-transparent" asChild>
309
+ <Link href="/profile">
310
+ <Clock className="mr-2 h-4 w-4 text-primary" />
311
+ Update Personal Details
312
+ </Link>
313
+ </Button>
314
+ </CardContent>
315
+ </Card>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </main>
320
+ <Footer />
321
+ </div>
322
+ )
323
+ }
Frontend/app/globals.css ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ :root {
7
+ --background: oklch(0.99 0.01 240);
8
+ --foreground: oklch(0.1 0.02 240);
9
+ --card: oklch(1 0 0);
10
+ --card-foreground: oklch(0.1 0.02 240);
11
+ --popover: oklch(1 0 0);
12
+ --popover-foreground: oklch(0.1 0.02 240);
13
+ /* deep blue primary */
14
+ --primary: oklch(0.25 0.05 240);
15
+ --primary-foreground: oklch(0.98 0.01 240);
16
+ /* bright blue secondary */
17
+ --secondary: oklch(0.6 0.15 240);
18
+ --secondary-foreground: oklch(1 0 0);
19
+ --muted: oklch(0.95 0.01 240);
20
+ --muted-foreground: oklch(0.4 0.02 240);
21
+ /* gold accent */
22
+ --accent: oklch(0.75 0.12 85);
23
+ --accent-foreground: oklch(0.2 0.05 85);
24
+ --destructive: oklch(0.6 0.2 25);
25
+ --destructive-foreground: oklch(0.98 0.01 25);
26
+ --success: oklch(0.65 0.15 150);
27
+ --success-foreground: oklch(0.98 0.01 150);
28
+ --border: oklch(0.9 0.01 240);
29
+ --input: oklch(0.9 0.01 240);
30
+ --ring: oklch(0.6 0.15 240);
31
+ --radius: 0.75rem;
32
+ --sidebar: oklch(0.985 0 0);
33
+ --sidebar-foreground: oklch(0.145 0 0);
34
+ --sidebar-primary: oklch(0.205 0 0);
35
+ --sidebar-primary-foreground: oklch(0.985 0 0);
36
+ --sidebar-accent: oklch(0.97 0 0);
37
+ --sidebar-accent-foreground: oklch(0.205 0 0);
38
+ --sidebar-border: oklch(0.922 0 0);
39
+ --sidebar-ring: oklch(0.708 0 0);
40
+ }
41
+
42
+ .dark {
43
+ --background: oklch(0.145 0 0);
44
+ --foreground: oklch(0.985 0 0);
45
+ --card: oklch(0.145 0 0);
46
+ --card-foreground: oklch(0.985 0 0);
47
+ --popover: oklch(0.145 0 0);
48
+ --popover-foreground: oklch(0.985 0 0);
49
+ --primary: oklch(0.985 0 0);
50
+ --primary-foreground: oklch(0.205 0 0);
51
+ --secondary: oklch(0.269 0 0);
52
+ --secondary-foreground: oklch(0.985 0 0);
53
+ --muted: oklch(0.269 0 0);
54
+ --muted-foreground: oklch(0.708 0 0);
55
+ --accent: oklch(0.269 0 0);
56
+ --accent-foreground: oklch(0.985 0 0);
57
+ --destructive: oklch(0.396 0.141 25.723);
58
+ --destructive-foreground: oklch(0.637 0.237 25.331);
59
+ --border: oklch(0.269 0 0);
60
+ --input: oklch(0.269 0 0);
61
+ --ring: oklch(0.439 0 0);
62
+ --chart-1: oklch(0.488 0.243 264.376);
63
+ --chart-2: oklch(0.696 0.17 162.48);
64
+ --chart-3: oklch(0.769 0.188 70.08);
65
+ --chart-4: oklch(0.627 0.265 303.9);
66
+ --chart-5: oklch(0.645 0.246 16.439);
67
+ --sidebar: oklch(0.205 0 0);
68
+ --sidebar-foreground: oklch(0.985 0 0);
69
+ --sidebar-primary: oklch(0.488 0.243 264.376);
70
+ --sidebar-primary-foreground: oklch(0.985 0 0);
71
+ --sidebar-accent: oklch(0.269 0 0);
72
+ --sidebar-accent-foreground: oklch(0.985 0 0);
73
+ --sidebar-border: oklch(0.269 0 0);
74
+ --sidebar-ring: oklch(0.439 0 0);
75
+ }
76
+
77
+ @theme inline {
78
+ --font-sans: "Geist", "Geist Fallback";
79
+ --font-mono: "Geist Mono", "Geist Mono Fallback";
80
+ --color-background: var(--background);
81
+ --color-foreground: var(--foreground);
82
+ --color-card: var(--card);
83
+ --color-card-foreground: var(--card-foreground);
84
+ --color-popover: var(--popover);
85
+ --color-popover-foreground: var(--popover-foreground);
86
+ --color-primary: var(--primary);
87
+ --color-primary-foreground: var(--primary-foreground);
88
+ --color-secondary: var(--secondary);
89
+ --color-secondary-foreground: var(--secondary-foreground);
90
+ --color-muted: var(--muted);
91
+ --color-muted-foreground: var(--muted-foreground);
92
+ --color-accent: var(--accent);
93
+ --color-accent-foreground: var(--accent-foreground);
94
+ --color-destructive: var(--destructive);
95
+ --color-destructive-foreground: var(--destructive-foreground);
96
+ --color-border: var(--border);
97
+ --color-input: var(--input);
98
+ --color-ring: var(--ring);
99
+ --color-success: var(--success);
100
+ --color-success-foreground: var(--success-foreground);
101
+
102
+ /* Custom animations for chat and micro-interactions */
103
+ --animate-in: in 0.3s ease-out;
104
+ --animate-out: out 0.3s ease-in;
105
+ --animate-fade-in: fade-in 0.5s ease-out;
106
+ --animate-slide-up: slide-up 0.4s ease-out;
107
+ --color-chart-1: var(--chart-1);
108
+ --color-chart-2: var(--chart-2);
109
+ --color-chart-3: var(--chart-3);
110
+ --color-chart-4: var(--chart-4);
111
+ --color-chart-5: var(--chart-5);
112
+ --radius-sm: calc(var(--radius) - 4px);
113
+ --radius-md: calc(var(--radius) - 2px);
114
+ --radius-lg: var(--radius);
115
+ --radius-xl: calc(var(--radius) + 4px);
116
+ --color-sidebar: var(--sidebar);
117
+ --color-sidebar-foreground: var(--sidebar-foreground);
118
+ --color-sidebar-primary: var(--sidebar-primary);
119
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
120
+ --color-sidebar-accent: var(--sidebar-accent);
121
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
122
+ --color-sidebar-border: var(--sidebar-border);
123
+ --color-sidebar-ring: var(--sidebar-ring);
124
+ }
125
+
126
+ @keyframes fade-in {
127
+ from {
128
+ opacity: 0;
129
+ }
130
+ to {
131
+ opacity: 1;
132
+ }
133
+ }
134
+
135
+ @keyframes slide-up {
136
+ from {
137
+ transform: translateY(10px);
138
+ opacity: 0;
139
+ }
140
+ to {
141
+ transform: translateY(0);
142
+ opacity: 1;
143
+ }
144
+ }
145
+
146
+ @layer base {
147
+ * {
148
+ @apply border-border outline-ring/50;
149
+ }
150
+ body {
151
+ @apply bg-background text-foreground selection:bg-accent/30 px-4 sm:px-6 lg:px-8;
152
+ }
153
+
154
+ /* Custom patterns for Nepali cultural feel */
155
+ .nepali-pattern {
156
+ background-image: radial-gradient(var(--accent) 0.5px, transparent 0.5px);
157
+ background-size: 20px 20px;
158
+ opacity: 0.05;
159
+ }
160
+ }
Frontend/app/layout.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type React from "react"
2
+ import type { Metadata } from "next"
3
+ import { Geist, Geist_Mono } from "next/font/google"
4
+ import "./globals.css"
5
+ import { AuthProvider } from "@/context/auth-context"
6
+ import { Toaster } from "@/components/ui/toaster"
7
+
8
+ const _geist = Geist({ subsets: ["latin"] })
9
+ const _geistMono = Geist_Mono({ subsets: ["latin"] })
10
+
11
+ export const metadata: Metadata = {
12
+ title: "Setu",
13
+ description: "Your digital legal assistant for legal welfare and rights in Nepal.",
14
+ // icons: {
15
+ // icon: [
16
+ // {
17
+ // url: "/icon-light-32x32.png",
18
+ // media: "(prefers-color-scheme: light)",
19
+ // },
20
+ // {
21
+ // url: "/icon-dark-32x32.png",
22
+ // media: "(prefers-color-scheme: dark)",
23
+ // },
24
+ // {
25
+ // url: "/icon.svg",
26
+ // type: "image/svg+xml",
27
+ // },
28
+ // ],
29
+ // apple: "/apple-icon.png",
30
+ // },
31
+ }
32
+
33
+ export default function RootLayout({
34
+ children,
35
+ }: Readonly<{
36
+ children: React.ReactNode
37
+ }>) {
38
+ return (
39
+ <html lang="en" suppressHydrationWarning>
40
+ <body className={`font-sans antialiased min-h-screen flex flex-col`}>
41
+ <AuthProvider>
42
+ {children}
43
+ <Toaster />
44
+ </AuthProvider>
45
+ </body>
46
+ </html>
47
+ )
48
+ }
Frontend/app/letter-generator/page.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { LetterGenerator } from "@/components/chatbot/letter-generator"
4
+ import { Navbar } from "@/components/layout/navbar"
5
+ import { Footer } from "@/components/layout/footer"
6
+ import { useAuthGuard } from "@/hooks/use-auth-guard"
7
+
8
+ export default function LetterGeneratorPage() {
9
+ const { isLoading, isAuthenticated } = useAuthGuard()
10
+
11
+ if (isLoading) {
12
+ return (
13
+ <div className="flex flex-col min-h-screen">
14
+ <Navbar />
15
+ <main className="flex-1 flex items-center justify-center">
16
+ <div className="text-center">
17
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
18
+ <p className="text-muted-foreground">Checking authentication...</p>
19
+ </div>
20
+ </main>
21
+ <Footer />
22
+ </div>
23
+ )
24
+ }
25
+
26
+ if (!isAuthenticated) {
27
+ return null // Will redirect to login
28
+ }
29
+
30
+ return (
31
+ <>
32
+ {/* Load Nepali fonts for letter generation */}
33
+ <link
34
+ rel="preconnect"
35
+ href="https://fonts.googleapis.com"
36
+ />
37
+ <link
38
+ rel="preconnect"
39
+ href="https://fonts.gstatic.com"
40
+ crossOrigin="anonymous"
41
+ />
42
+ <link
43
+ href="https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@400;600;700&display=swap"
44
+ rel="stylesheet"
45
+ />
46
+
47
+ <div className="flex flex-col min-h-screen">
48
+ <Navbar />
49
+ <main className="flex-1 p-4 md:p-8 bg-muted/20">
50
+ <div className="container mx-auto max-w-7xl h-full">
51
+ <div className="mb-6">
52
+ <h1 className="text-2xl font-bold text-primary">Letter Generation Assistant</h1>
53
+ <p className="text-sm text-muted-foreground">
54
+ Generate official letters for various purposes with AI assistance
55
+ </p>
56
+ </div>
57
+ <LetterGenerator />
58
+ </div>
59
+ </main>
60
+ <Footer />
61
+ </div>
62
+ </>
63
+ )
64
+ }
Frontend/app/loading.tsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export default function Loading() {
2
+ return null
3
+ }
Frontend/app/login/page.tsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import type React from "react"
4
+ import { useState } from "react"
5
+ import { useRouter } from "next/navigation"
6
+ import { Button } from "@/components/ui/button"
7
+ import { Input } from "@/components/ui/input"
8
+ import { Label } from "@/components/ui/label"
9
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
10
+ import { Navbar } from "@/components/layout/navbar"
11
+ import { Footer } from "@/components/layout/footer"
12
+ import Link from "next/link"
13
+ import { Scale, AlertCircle } from "lucide-react"
14
+ import { Alert, AlertDescription } from "@/components/ui/alert"
15
+ import { useAuth } from "@/context/auth-context"
16
+ import { apiClient } from "@/lib/api-client"
17
+
18
+ export default function LoginPage() {
19
+ const [email, setEmail] = useState("")
20
+ const [password, setPassword] = useState("")
21
+ const [error, setError] = useState<string | null>(null)
22
+ const [isLoading, setIsLoading] = useState(false)
23
+ const router = useRouter()
24
+ const { login } = useAuth()
25
+
26
+ const handleSubmit = async (e: React.FormEvent) => {
27
+ e.preventDefault()
28
+ setError(null)
29
+ setIsLoading(true)
30
+
31
+ try {
32
+ // Call backend login endpoint using API client
33
+ const data = await apiClient.login(email, password)
34
+
35
+ console.log("Login response:", data)
36
+ console.log("User data from backend:", data.user)
37
+
38
+ // Store token and user data in localStorage
39
+ localStorage.setItem("access_token", data.access_token)
40
+ if (data.user) {
41
+ localStorage.setItem("user", JSON.stringify(data.user))
42
+ console.log("Stored user in localStorage:", localStorage.getItem("user"))
43
+ }
44
+
45
+ // Update auth context with the new token
46
+ login(data.access_token)
47
+
48
+ // Check for redirect parameter
49
+ const searchParams = new URLSearchParams(window.location.search)
50
+ const redirectTo = searchParams.get("redirect")
51
+
52
+ // Redirect to the original page or dashboard
53
+ router.push(redirectTo || "/dashboard")
54
+ } catch (err) {
55
+ if (err instanceof Error) {
56
+ setError(err.message)
57
+ } else {
58
+ setError("Server connection failed. Please try again later.")
59
+ }
60
+ } finally {
61
+ setIsLoading(false)
62
+ }
63
+ }
64
+
65
+ return (
66
+ <div className="flex flex-col min-h-screen">
67
+ <Navbar />
68
+ <main className="flex-1 flex items-center justify-center p-4 bg-muted/20">
69
+ <Card className="w-full max-w-md border-primary/20 shadow-lg">
70
+ <CardHeader className="text-center">
71
+ <div className="flex justify-center mb-4">
72
+ <div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center text-primary">
73
+ <Scale className="h-6 w-6" />
74
+ </div>
75
+ </div>
76
+ <CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
77
+ <CardDescription>Enter your email to access your legal dashboard</CardDescription>
78
+ </CardHeader>
79
+ <form onSubmit={handleSubmit}>
80
+ <CardContent className="space-y-4">
81
+
82
+ {/* --- ERROR MESSAGE DISPLAY --- */}
83
+ {error && (
84
+ <Alert variant="destructive" className="bg-red-50 text-red-900 border-red-200">
85
+ <AlertCircle className="h-4 w-4" />
86
+ <AlertDescription>{error}</AlertDescription>
87
+ </Alert>
88
+ )}
89
+
90
+ <div className="space-y-2">
91
+ <Label htmlFor="email">Email</Label>
92
+ <Input
93
+ id="email"
94
+ name="email"
95
+ type="email"
96
+ placeholder="name@example.com"
97
+ required
98
+ value={email}
99
+ onChange={(e) => setEmail(e.target.value)}
100
+ />
101
+ </div>
102
+ <div className="space-y-2">
103
+ <Label htmlFor="password">Password</Label>
104
+ <Input
105
+ id="password"
106
+ name="password"
107
+ type="password"
108
+ required
109
+ value={password}
110
+ onChange={(e) => setPassword(e.target.value)}
111
+ />
112
+ </div>
113
+ </CardContent>
114
+ <CardFooter className="flex flex-col gap-4 pt-4">
115
+ <Button type="submit" className="w-full bg-primary" disabled={isLoading}>
116
+ {isLoading ? "Signing In..." : "Sign In"}
117
+ </Button>
118
+ <p className="text-sm text-center text-muted-foreground">
119
+ Don&apos;t have an account?{" "}
120
+ <Link href="/register" className="text-primary hover:underline">
121
+ Register here
122
+ </Link>
123
+ </p>
124
+ </CardFooter>
125
+ </form>
126
+ </Card>
127
+ </main>
128
+ <Footer />
129
+ </div>
130
+ )
131
+ }
Frontend/app/page.tsx ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Navbar } from "@/components/layout/navbar"
2
+ import { Footer } from "@/components/layout/footer"
3
+ import { Button } from "@/components/ui/button"
4
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"
5
+ import { MessageSquare, Search, BookOpen, CheckCircle } from "lucide-react"
6
+ import Link from "next/link"
7
+
8
+ export default function Home() {
9
+ return (
10
+ <div className="flex flex-col min-h-screen">
11
+ <Navbar />
12
+ <main className="flex-1">
13
+ {/* Hero Section */}
14
+ <section className="relative py-20 md:py-32 overflow-hidden">
15
+ <div className="absolute inset-0 nepali-pattern -z-10" />
16
+ <div className="container max-w-7xl mx-auto relative text-center space-y-8">
17
+ {/* <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary text-xs font-semibold mb-4 border border-primary/20">
18
+ <CheckCircle className="h-3 w-3" />
19
+ Trusted by 5,000+ Nepali Citizens
20
+ </div> */}
21
+ <h1 className="text-4xl md:text-6xl font-extrabold tracking-tight text-balance">
22
+ Know Your Rights - <span className="text-primary">Nepal Legal Assistance</span>
23
+ </h1>
24
+ <p className="max-w-[700px] mx-auto text-muted-foreground md:text-xl text-pretty">
25
+ Your digital gateway to legal justice in Nepal. Access our AI-powered law assistant, analyze legal notices
26
+ for bias, and learn about your fundamental rights.
27
+ </p>
28
+ <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
29
+ <Button size="lg" className="h-12 px-8 bg-primary hover:bg-primary/90" asChild>
30
+ <Link href="/chatbot">Start Legal Consultation</Link>
31
+ </Button>
32
+ <Button
33
+ size="lg"
34
+ variant="outline"
35
+ className="h-12 px-8 border-primary text-primary hover:bg-primary/5 bg-transparent"
36
+ asChild
37
+ >
38
+ <Link href="/bias-checker">Analyze Legal Documents</Link>
39
+ </Button>
40
+ </div>
41
+ </div>
42
+ </section>
43
+
44
+ {/* Features Grid */}
45
+ <section className="py-20 bg-muted/30">
46
+ <div className="container max-w-7xl mx-auto">
47
+ <div className="text-center mb-16 space-y-4">
48
+ <h2 className="text-3xl font-bold tracking-tight">Comprehensive Legal Services</h2>
49
+ <p className="text-muted-foreground max-w-[600px] mx-auto">
50
+ Tailored solutions for every legal need, built with privacy and accessibility in mind.
51
+ </p>
52
+ </div>
53
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
54
+ <Card className="hover:shadow-lg transition-all border-primary/10">
55
+ <CardHeader>
56
+ <div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
57
+ <MessageSquare className="h-6 w-6 text-primary" />
58
+ </div>
59
+ <CardTitle>Law Chatbot</CardTitle>
60
+ <CardDescription>Get instant answers to your legal queries based on Nepali law.</CardDescription>
61
+ </CardHeader>
62
+ <CardContent>
63
+ <Button variant="link" className="p-0 h-auto text-primary" asChild>
64
+ <Link href="/chatbot">Learn more →</Link>
65
+ </Button>
66
+ </CardContent>
67
+ </Card>
68
+
69
+ <Card className="hover:shadow-lg transition-all border-primary/10">
70
+ <CardHeader>
71
+ <div className="h-12 w-12 rounded-lg bg-accent/10 flex items-center justify-center mb-4">
72
+ <Search className="h-6 w-6 text-accent" />
73
+ </div>
74
+ <CardTitle>Bias Checker</CardTitle>
75
+ <CardDescription>
76
+ Analyze legal notices and contracts for potential linguistic biases.
77
+ </CardDescription>
78
+ </CardHeader>
79
+ <CardContent>
80
+ <Button variant="link" className="p-0 h-auto text-accent" asChild>
81
+ <Link href="/bias-checker">Try it now →</Link>
82
+ </Button>
83
+ </CardContent>
84
+ </Card>
85
+
86
+ <Card className="hover:shadow-lg transition-all border-primary/10">
87
+ <CardHeader>
88
+ <div className="h-12 w-12 rounded-lg bg-success/10 flex items-center justify-center mb-4">
89
+ <BookOpen className="h-6 w-6 text-success" />
90
+ </div>
91
+ <CardTitle>Letter Generator</CardTitle>
92
+ <CardDescription>
93
+ Generate professional government letters effortlessly. Describe what you need, provide the required information, and get an official letter tailored to your reques
94
+ </CardDescription>
95
+ </CardHeader>
96
+ <CardContent>
97
+ <Button variant="link" className="p-0 h-auto text-success" asChild>
98
+ <Link href="/letter-generator">Generate Letters →</Link>
99
+ </Button>
100
+ </CardContent>
101
+ </Card>
102
+ </div>
103
+ </div>
104
+ </section>
105
+
106
+ {/* It is the section that shows the users stats */}
107
+ {/* Statistics Section */}
108
+ {/* <section className="py-20 border-y">
109
+ <div className="container grid grid-cols-2 md:grid-cols-4 gap-8 text-center">
110
+ <div className="space-y-2">
111
+ <div className="text-4xl font-bold text-primary">15k+</div>
112
+ <p className="text-sm text-muted-foreground uppercase tracking-wider font-semibold">Users Helped</p>
113
+ </div>
114
+ <div className="space-y-2">
115
+ <div className="text-4xl font-bold text-primary">50k+</div>
116
+ <p className="text-sm text-muted-foreground uppercase tracking-wider font-semibold">Queries Answered</p>
117
+ </div>
118
+ <div className="space-y-2">
119
+ <div className="text-4xl font-bold text-primary">2.5k+</div>
120
+ <p className="text-sm text-muted-foreground uppercase tracking-wider font-semibold">Docs Analyzed</p>
121
+ </div>
122
+ <div className="space-y-2">
123
+ <div className="text-4xl font-bold text-primary">98%</div>
124
+ <p className="text-sm text-muted-foreground uppercase tracking-wider font-semibold">Satisfaction Rate</p>
125
+ </div>
126
+ </div>
127
+ </section> */}
128
+
129
+ {/* CTA Section */}
130
+ {/** This is the section we can add to get the Create account section */}
131
+ {/* <section className="py-20 bg-primary text-primary-foreground relative overflow-hidden">
132
+ <div className="absolute inset-0 opacity-10 nepali-pattern" />
133
+ <div className="container relative text-center space-y-6">
134
+ <h2 className="text-3xl md:text-4xl font-bold">Ready to take control of your legal journey?</h2>
135
+ <p className="max-w-[600px] mx-auto opacity-90">
136
+ Join thousands of Nepali citizens who are using our platform to understand their rights and seek justice.
137
+ </p>
138
+ <Button size="lg" variant="secondary" className="h-12 px-8" asChild>
139
+ <Link href="/register">Create Your Account</Link>
140
+ </Button>
141
+ </div>
142
+ </section> */}
143
+ </main>
144
+ <Footer />
145
+ </div>
146
+ )
147
+ }
Frontend/app/profile/page.tsx ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { useAuth } from "@/context/auth-context"
5
+ import { Navbar } from "@/components/layout/navbar"
6
+ import { Footer } from "@/components/layout/footer"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card"
9
+ import { Input } from "@/components/ui/input"
10
+ import { Label } from "@/components/ui/label"
11
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
12
+ import { useToast } from "@/hooks/use-toast"
13
+ import { redirect } from "next/navigation"
14
+
15
+ export default function ProfilePage() {
16
+ const { user, isLoading, updateUserDetails } = useAuth()
17
+ const { toast } = useToast()
18
+ const [age, setAge] = useState("")
19
+ const [nid, setNid] = useState("")
20
+ const [nidError, setNidError] = useState("")
21
+
22
+ useEffect(() => {
23
+ if (user?.details?.age) {
24
+ setAge(user.details.age.toString())
25
+ }
26
+ if (user?.details?.nid) {
27
+ setNid(user.details.nid)
28
+ }
29
+ }, [user])
30
+
31
+ if (isLoading) return null
32
+ if (!user) redirect("/login")
33
+
34
+ // Get the first letter of the user's name
35
+ const getInitial = () => {
36
+ if (user?.name) {
37
+ return user.name.charAt(0).toUpperCase()
38
+ }
39
+ return "U"
40
+ }
41
+
42
+ // Handle age input - only allow numbers, max 3 digits
43
+ const handleAgeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
44
+ const value = e.target.value
45
+ // Only allow numbers and limit to 3 digits
46
+ if (value === "" || (/^\d+$/.test(value) && value.length <= 3)) {
47
+ setAge(value)
48
+ }
49
+ }
50
+
51
+ // Handle NID input with auto-hyphen formatting (xxx-xxx-xxx-x)
52
+ const handleNidChange = (e: React.ChangeEvent<HTMLInputElement>) => {
53
+ let value = e.target.value
54
+ // Remove all non-digit characters
55
+ value = value.replace(/\D/g, "")
56
+
57
+ // Limit to 10 digits
58
+ if (value.length > 10) {
59
+ value = value.slice(0, 10)
60
+ }
61
+
62
+ // Format with hyphens: xxx-xxx-xxx-x
63
+ let formatted = ""
64
+ if (value.length > 0) {
65
+ formatted = value.slice(0, 3)
66
+ if (value.length > 3) {
67
+ formatted += "-" + value.slice(3, 6)
68
+ }
69
+ if (value.length > 6) {
70
+ formatted += "-" + value.slice(6, 9)
71
+ }
72
+ if (value.length > 9) {
73
+ formatted += "-" + value.slice(9, 10)
74
+ }
75
+ }
76
+
77
+ setNid(formatted)
78
+
79
+ // Validate NID format
80
+ if (formatted && formatted.length < 13) {
81
+ setNidError("NID must be in format: xxx-xxx-xxx-x")
82
+ } else {
83
+ setNidError("")
84
+ }
85
+ }
86
+
87
+ // Handle form submission
88
+ const handleSave = async () => {
89
+ // Validate NID format before saving
90
+ if (nid && nid.length !== 13) {
91
+ toast({
92
+ title: "Error",
93
+ description: "NID must be in format: xxx-xxx-xxx-x (10 digits)",
94
+ variant: "destructive",
95
+ })
96
+ return
97
+ }
98
+
99
+ // Validate age
100
+ if (age && (parseInt(age) < 0 || parseInt(age) > 150)) {
101
+ toast({
102
+ title: "Error",
103
+ description: "Please enter a valid age",
104
+ variant: "destructive",
105
+ })
106
+ return
107
+ }
108
+
109
+ // Update user details in auth context
110
+ const updatedDetails = {
111
+ ...user.details,
112
+ nid: nid || user.details?.nid,
113
+ age: age ? parseInt(age) : user.details?.age,
114
+ }
115
+
116
+ await updateUserDetails(updatedDetails)
117
+
118
+ // Here you would typically save to backend
119
+ toast({
120
+ title: "Success",
121
+ description: "Profile updated successfully!",
122
+ })
123
+ }
124
+
125
+ return (
126
+ <div className="flex flex-col min-h-screen">
127
+ <Navbar />
128
+ <main className="flex-1 p-4 md:p-8 bg-muted/20">
129
+ <div className="container mx-auto max-w-2xl">
130
+ <div className="mb-8">
131
+ <h1 className="text-3xl font-bold text-primary tracking-tight">Profile</h1>
132
+ <p className="text-muted-foreground">View and manage your personal information</p>
133
+ </div>
134
+
135
+ <Card className="border-primary/10">
136
+ <CardHeader>
137
+ <div className="flex flex-col items-center gap-4 text-center">
138
+ <Avatar className="h-32 w-32 border-4 border-primary">
139
+ <AvatarFallback className="bg-primary text-primary-foreground text-5xl">
140
+ {getInitial()}
141
+ </AvatarFallback>
142
+ </Avatar>
143
+ <div>
144
+ <CardTitle className="text-2xl">{user.name}</CardTitle>
145
+ <CardDescription className="text-base mt-1">{user.email}</CardDescription>
146
+
147
+ </div>
148
+ </div>
149
+ </CardHeader>
150
+ <CardContent className="space-y-6">
151
+
152
+ <div className="space-y-2">
153
+ <Label htmlFor="age">Age</Label>
154
+ <Input
155
+ id="age"
156
+ type="text"
157
+ value={age}
158
+ onChange={handleAgeChange}
159
+ placeholder="Enter your age"
160
+ className="max-w-xs"
161
+ />
162
+ <p className="text-xs text-muted-foreground">Enter numbers only</p>
163
+ </div>
164
+
165
+ <div className="space-y-2">
166
+ <Label htmlFor="nid">National ID (NID)</Label>
167
+ <Input
168
+ id="nid"
169
+ type="text"
170
+ value={nid}
171
+ onChange={handleNidChange}
172
+ placeholder="xxx-xxx-xxx-x"
173
+ maxLength={13}
174
+ />
175
+ {nidError && (
176
+ <p className="text-xs text-destructive">{nidError}</p>
177
+ )}
178
+ <p className="text-xs text-muted-foreground">Format: xxx-xxx-xxx-x (10 digits)</p>
179
+ </div>
180
+
181
+ <div className="pt-4">
182
+ <Button
183
+ onClick={handleSave}
184
+ className="w-full bg-primary hover:bg-primary/90"
185
+ disabled={!!nidError && nid.length > 0}
186
+ >
187
+ Save Changes
188
+ </Button>
189
+ </div>
190
+ </CardContent>
191
+ </Card>
192
+ </div>
193
+ </main>
194
+ <Footer />
195
+ </div>
196
+ )
197
+ }
Frontend/app/register/page.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RegisterForm } from "@/components/auth/register-form"
2
+ import { Navbar } from "@/components/layout/navbar"
3
+ import { Footer } from "@/components/layout/footer"
4
+
5
+ export default function RegisterPage() {
6
+ return (
7
+ <div className="flex flex-col min-h-screen">
8
+ <Navbar />
9
+ <main className="flex-1 flex items-center justify-center p-4 md:p-8 bg-muted/20">
10
+ <div className="w-full max-w-2xl">
11
+ <RegisterForm />
12
+ </div>
13
+ </main>
14
+ <Footer />
15
+ </div>
16
+ )
17
+ }
Frontend/components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
Frontend/components/auth/register-form.tsx ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import type React from "react"
4
+ import { useState } from "react"
5
+ import { useRouter } from "next/navigation"
6
+ import { Button } from "@/components/ui/button"
7
+ import { Input } from "@/components/ui/input"
8
+ import { Label } from "@/components/ui/label"
9
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
10
+ import { useToast } from "@/hooks/use-toast"
11
+ import { Scale, Eye, EyeOff } from "lucide-react"
12
+ import { apiClient } from "@/lib/api-client"
13
+ import { setAuthData } from "@/lib/auth-utils"
14
+ import { useAuth } from "@/context/auth-context"
15
+
16
+ export function RegisterForm() {
17
+ const [formData, setFormData] = useState({
18
+ name: "",
19
+ email: "",
20
+ password: "",
21
+ confirmPassword: "",
22
+ })
23
+ const [showPassword, setShowPassword] = useState(false)
24
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false)
25
+ const [isLoading, setIsLoading] = useState(false)
26
+ const router = useRouter()
27
+ const { toast } = useToast()
28
+ const { login } = useAuth()
29
+
30
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
31
+ setFormData({ ...formData, [e.target.name]: e.target.value })
32
+ }
33
+
34
+ const handleSubmit = async (e: React.FormEvent) => {
35
+ e.preventDefault()
36
+
37
+ // Validate password match
38
+ if (formData.password !== formData.confirmPassword) {
39
+ toast({
40
+ title: "Error",
41
+ description: "Passwords do not match",
42
+ variant: "destructive",
43
+ })
44
+ return
45
+ }
46
+
47
+ // Validate password length
48
+ if (formData.password.length < 6) {
49
+ toast({
50
+ title: "Error",
51
+ description: "Password must be at least 6 characters long",
52
+ variant: "destructive",
53
+ })
54
+ return
55
+ }
56
+
57
+ setIsLoading(true)
58
+
59
+ try {
60
+ // Call the backend API
61
+ const response = await apiClient.register(
62
+ formData.email,
63
+ formData.password,
64
+ formData.name
65
+ )
66
+
67
+ console.log("Registration response:", response)
68
+
69
+ // Store the access token and user data in localStorage
70
+ if (response.access_token) {
71
+ setAuthData(response.access_token, response.refresh_token, response.user)
72
+
73
+ // Update auth context
74
+ login(response.access_token)
75
+
76
+ console.log("Stored user data:", localStorage.getItem("user"))
77
+
78
+ toast({
79
+ title: "Success",
80
+ description: "Account created successfully! Redirecting...",
81
+ })
82
+
83
+ // Redirect to dashboard or home page after successful registration
84
+ setTimeout(() => {
85
+ router.push("/dashboard")
86
+ }, 1500)
87
+ }
88
+ } catch (error) {
89
+ console.error("Registration error:", error)
90
+ toast({
91
+ title: "Registration Failed",
92
+ description: error instanceof Error ? error.message : "An error occurred during registration",
93
+ variant: "destructive",
94
+ })
95
+ } finally {
96
+ setIsLoading(false)
97
+ }
98
+ }
99
+
100
+ return (
101
+ <Card className="w-full max-w-md mx-auto border-primary/20 shadow-xl overflow-hidden">
102
+ <CardHeader className="text-center pt-8">
103
+ <div className="flex justify-center mb-4">
104
+ <div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center text-primary">
105
+ <Scale className="h-6 w-6" />
106
+ </div>
107
+ </div>
108
+ <CardTitle className="text-2xl font-bold">Create Your Account</CardTitle>
109
+ <CardDescription>
110
+ Enter your basic information to get started
111
+ </CardDescription>
112
+ </CardHeader>
113
+
114
+ <form onSubmit={handleSubmit}>
115
+ <CardContent className="space-y-4 pt-4">
116
+ <div className="space-y-2">
117
+ <Label htmlFor="name">Full Name</Label>
118
+ <Input
119
+ name="name"
120
+ id="name"
121
+ placeholder="John Doe"
122
+ required
123
+ value={formData.name}
124
+ onChange={handleInputChange}
125
+ />
126
+ </div>
127
+
128
+ <div className="space-y-2">
129
+ <Label htmlFor="email">Email Address</Label>
130
+ <Input
131
+ name="email"
132
+ id="email"
133
+ type="email"
134
+ placeholder="john@example.com"
135
+ required
136
+ value={formData.email}
137
+ onChange={handleInputChange}
138
+ />
139
+ </div>
140
+
141
+ <div className="space-y-2">
142
+ <Label htmlFor="password">Password</Label>
143
+ <div className="relative">
144
+ <Input
145
+ name="password"
146
+ id="password"
147
+ type={showPassword ? "text" : "password"}
148
+ required
149
+ value={formData.password}
150
+ onChange={handleInputChange}
151
+ className="pr-10"
152
+ />
153
+ <button
154
+ type="button"
155
+ onClick={() => setShowPassword(!showPassword)}
156
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
157
+ >
158
+ {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
159
+ </button>
160
+ </div>
161
+ </div>
162
+
163
+ <div className="space-y-2">
164
+ <Label htmlFor="confirmPassword">Confirm Password</Label>
165
+ <div className="relative">
166
+ <Input
167
+ name="confirmPassword"
168
+ id="confirmPassword"
169
+ type={showConfirmPassword ? "text" : "password"}
170
+ required
171
+ value={formData.confirmPassword}
172
+ onChange={handleInputChange}
173
+ className="pr-10"
174
+ />
175
+ <button
176
+ type="button"
177
+ onClick={() => setShowConfirmPassword(!showConfirmPassword)}
178
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
179
+ >
180
+ {showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
181
+ </button>
182
+ </div>
183
+ </div>
184
+ </CardContent>
185
+
186
+ <CardFooter className="flex justify-center pb-8">
187
+ <Button type="submit" className="w-full bg-primary hover:bg-primary/90" disabled={isLoading}>
188
+ {isLoading ? "Creating Account..." : "Create Account"}
189
+ </Button>
190
+ </CardFooter>
191
+ </form>
192
+ </Card>
193
+ )
194
+ }
Frontend/components/chatbot/bias-checker.tsx ADDED
@@ -0,0 +1,805 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useRef } from "react"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Textarea } from "@/components/ui/textarea"
6
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
7
+ import { Progress } from "@/components/ui/progress"
8
+ import { Badge } from "@/components/ui/badge"
9
+ import { AlertCircle, Info, Search, Upload, X, CheckCircle2, ShieldAlert, FileText, Download, RefreshCw, ThumbsUp, ThumbsDown } from "lucide-react"
10
+ import { cn } from "@/lib/utils"
11
+ import { addDocumentToCache } from "@/lib/document-cache"
12
+
13
+ interface BiasedSentence {
14
+ original: string
15
+ category: string
16
+ confidence: number
17
+ suggestion: string | null
18
+ explanation: string | null
19
+ success: boolean
20
+ }
21
+
22
+ interface BiasAnalysisResult {
23
+ success: boolean
24
+ biasedCount: number
25
+ unbiasedCount: number
26
+ totalSentences: number
27
+ biasedSentences: BiasedSentence[]
28
+ filename?: string
29
+ }
30
+
31
+ // HITL Interfaces
32
+ interface HITLReviewItem {
33
+ sentence_id: string
34
+ original_sentence: string
35
+ is_biased: boolean
36
+ category: string
37
+ confidence: number
38
+ suggestion: string | null
39
+ approved_suggestion: string | null
40
+ status: "pending" | "approved" | "needs_regeneration"
41
+ }
42
+
43
+ interface HITLSessionData {
44
+ success: boolean
45
+ session_id: string
46
+ total_sentences: number
47
+ biased_count: number
48
+ neutral_count: number
49
+ sentences: HITLReviewItem[]
50
+ filename: string
51
+ }
52
+
53
+ // Category display names in Nepali/English
54
+ const categoryLabels: Record<string, string> = {
55
+ neutral: "Neutral",
56
+ gender: "Gender Bias",
57
+ religional: "Religious Bias",
58
+ caste: "Caste Bias",
59
+ religion: "Religion Bias",
60
+ appearence: "Appearance Bias",
61
+ socialstatus: "Social Status Bias",
62
+ amiguity: "Ambiguity",
63
+ political: "Political Bias",
64
+ Age: "Age Bias",
65
+ Disablity: "Disability Bias",
66
+ }
67
+
68
+ export function BiasChecker() {
69
+ const [text, setText] = useState("")
70
+ const [file, setFile] = useState<File | null>(null)
71
+ const [isAnalyzing, setIsAnalyzing] = useState(false)
72
+ const [result, setResult] = useState<BiasAnalysisResult | null>(null)
73
+ const [dragActive, setDragActive] = useState(false)
74
+ const fileInputRef = useRef<HTMLInputElement>(null)
75
+
76
+ // HITL state
77
+ const [useHITL, setUseHITL] = useState(true) // Enable HITL by default for PDFs
78
+ const [hitlSession, setHitlSession] = useState<HITLSessionData | null>(null)
79
+ const [isGeneratingPDF, setIsGeneratingPDF] = useState(false)
80
+
81
+ const handleDrag = (e: React.DragEvent) => {
82
+ e.preventDefault()
83
+ e.stopPropagation()
84
+ if (e.type === "dragenter" || e.type === "dragover") {
85
+ setDragActive(true)
86
+ } else if (e.type === "dragleave") {
87
+ setDragActive(false)
88
+ }
89
+ }
90
+
91
+ const handleDrop = (e: React.DragEvent) => {
92
+ e.preventDefault()
93
+ setDragActive(false)
94
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
95
+ const selectedFile = e.dataTransfer.files[0]
96
+ if (selectedFile.type === "application/pdf" || selectedFile.name.endsWith(".pdf")) {
97
+ setFile(selectedFile)
98
+ setText("") // Clear text input when file is uploaded
99
+ }
100
+ }
101
+ }
102
+
103
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
104
+ if (e.target.files && e.target.files[0]) {
105
+ setFile(e.target.files[0])
106
+ setText("") // Clear text input when file is uploaded
107
+ }
108
+ }
109
+
110
+ const removeFile = () => {
111
+ setFile(null)
112
+ if (fileInputRef.current) fileInputRef.current.value = ""
113
+ }
114
+
115
+ const analyzeText = async () => {
116
+ if (!text.trim() && !file) return
117
+ setIsAnalyzing(true)
118
+ setResult(null)
119
+ setHitlSession(null)
120
+
121
+ try {
122
+ const formData = new FormData()
123
+
124
+ if (file) {
125
+ formData.append("file", file)
126
+ formData.append("use_hitl", useHITL ? "true" : "false")
127
+ } else {
128
+ formData.append("text", text)
129
+ formData.append("use_hitl", "false")
130
+ }
131
+
132
+ formData.append("confidence_threshold", "0.7")
133
+
134
+ const token = localStorage.getItem("access_token")
135
+ const headers: Record<string, string> = {}
136
+ if (token) {
137
+ headers["Authorization"] = `Bearer ${token}`
138
+ }
139
+
140
+ const response = await fetch("/api/bias-detection", {
141
+ method: "POST",
142
+ headers,
143
+ body: formData,
144
+ })
145
+
146
+ if (!response.ok) {
147
+ throw new Error("Failed to analyze bias")
148
+ }
149
+
150
+ const data = await response.json()
151
+
152
+ // Check if response is HITL session data
153
+ if (data.session_id && data.sentences) {
154
+ setHitlSession(data as HITLSessionData)
155
+
156
+ // Cache the analyzed document
157
+ if (file) {
158
+ addDocumentToCache({
159
+ filename: data.filename || file.name,
160
+ type: "bias-detection",
161
+ result: {
162
+ totalSentences: data.total_sentences,
163
+ biasedCount: data.biased_count,
164
+ neutralCount: data.neutral_count,
165
+ success: data.success,
166
+ },
167
+ sessionId: data.session_id,
168
+ })
169
+ }
170
+ } else {
171
+ setResult(data as BiasAnalysisResult)
172
+
173
+ // Cache the analyzed document
174
+ if (file) {
175
+ addDocumentToCache({
176
+ filename: data.filename || file.name,
177
+ type: "bias-detection",
178
+ result: {
179
+ totalSentences: data.totalSentences,
180
+ biasedCount: data.biasedCount,
181
+ neutralCount: data.unbiasedCount,
182
+ success: data.success,
183
+ },
184
+ })
185
+ }
186
+ }
187
+ } catch (error) {
188
+ console.error("[Bias Detection Error]:", error)
189
+ alert("Failed to analyze bias. Please ensure the backend server is running.")
190
+ } finally {
191
+ setIsAnalyzing(false)
192
+ }
193
+ }
194
+
195
+ const handleApproveSuggestion = async (sentenceId: string, suggestion: string) => {
196
+ if (!hitlSession) return
197
+
198
+ try {
199
+ const token = localStorage.getItem("access_token")
200
+ const headers: Record<string, string> = {
201
+ "Content-Type": "application/json"
202
+ }
203
+ if (token) {
204
+ headers["Authorization"] = `Bearer ${token}`
205
+ }
206
+
207
+ const response = await fetch("/api/bias-detection-hitl/approve", {
208
+ method: "POST",
209
+ headers,
210
+ body: JSON.stringify({
211
+ session_id: hitlSession.session_id,
212
+ sentence_id: sentenceId,
213
+ action: "approve",
214
+ approved_suggestion: suggestion
215
+ })
216
+ })
217
+
218
+ if (!response.ok) {
219
+ throw new Error("Failed to approve suggestion")
220
+ }
221
+
222
+ // Update local state
223
+ setHitlSession({
224
+ ...hitlSession,
225
+ sentences: hitlSession.sentences.map(s =>
226
+ s.sentence_id === sentenceId
227
+ ? { ...s, status: "approved", approved_suggestion: suggestion }
228
+ : s
229
+ )
230
+ })
231
+ } catch (error) {
232
+ console.error("[Approve Error]:", error)
233
+ alert("Failed to approve suggestion")
234
+ }
235
+ }
236
+
237
+ const handleRejectSuggestion = async (sentenceId: string) => {
238
+ if (!hitlSession) return
239
+
240
+ try {
241
+ const token = localStorage.getItem("access_token")
242
+ const headers: Record<string, string> = {
243
+ "Content-Type": "application/json"
244
+ }
245
+ if (token) {
246
+ headers["Authorization"] = `Bearer ${token}`
247
+ }
248
+
249
+ const response = await fetch("/api/bias-detection-hitl/approve", {
250
+ method: "POST",
251
+ headers,
252
+ body: JSON.stringify({
253
+ session_id: hitlSession.session_id,
254
+ sentence_id: sentenceId,
255
+ action: "reject"
256
+ })
257
+ })
258
+
259
+ if (!response.ok) {
260
+ throw new Error("Failed to reject suggestion")
261
+ }
262
+
263
+ // Update local state
264
+ setHitlSession({
265
+ ...hitlSession,
266
+ sentences: hitlSession.sentences.map(s =>
267
+ s.sentence_id === sentenceId
268
+ ? { ...s, status: "needs_regeneration" }
269
+ : s
270
+ )
271
+ })
272
+ } catch (error) {
273
+ console.error("[Reject Error]:", error)
274
+ alert("Failed to reject suggestion")
275
+ }
276
+ }
277
+
278
+ const handleRegenerateSuggestion = async (sentenceId: string) => {
279
+ if (!hitlSession) return
280
+
281
+ try {
282
+ const token = localStorage.getItem("access_token")
283
+ const headers: Record<string, string> = {
284
+ "Content-Type": "application/json"
285
+ }
286
+ if (token) {
287
+ headers["Authorization"] = `Bearer ${token}`
288
+ }
289
+
290
+ const response = await fetch("/api/bias-detection-hitl/regenerate", {
291
+ method: "POST",
292
+ headers,
293
+ body: JSON.stringify({
294
+ session_id: hitlSession.session_id,
295
+ sentence_id: sentenceId
296
+ })
297
+ })
298
+
299
+ if (!response.ok) {
300
+ throw new Error("Failed to regenerate suggestion")
301
+ }
302
+
303
+ const data = await response.json()
304
+
305
+ // Update local state with new suggestion
306
+ setHitlSession({
307
+ ...hitlSession,
308
+ sentences: hitlSession.sentences.map(s =>
309
+ s.sentence_id === sentenceId
310
+ ? { ...s, suggestion: data.new_suggestion, status: "pending" }
311
+ : s
312
+ )
313
+ })
314
+ } catch (error) {
315
+ console.error("[Regenerate Error]:", error)
316
+ alert("Failed to regenerate suggestion")
317
+ }
318
+ }
319
+
320
+ const handleGeneratePDF = async () => {
321
+ if (!hitlSession) return
322
+
323
+ setIsGeneratingPDF(true)
324
+ try {
325
+ const token = localStorage.getItem("access_token")
326
+ const headers: Record<string, string> = {
327
+ "Content-Type": "application/json"
328
+ }
329
+ if (token) {
330
+ headers["Authorization"] = `Bearer ${token}`
331
+ }
332
+
333
+ const response = await fetch("/api/bias-detection-hitl/generate-pdf", {
334
+ method: "POST",
335
+ headers,
336
+ body: JSON.stringify({
337
+ session_id: hitlSession.session_id
338
+ })
339
+ })
340
+
341
+ if (!response.ok) {
342
+ const errorData = await response.json().catch(() => ({}))
343
+ throw new Error(errorData.error || "Failed to generate PDF")
344
+ }
345
+
346
+ // Download the PDF
347
+ const blob = await response.blob()
348
+ const url = window.URL.createObjectURL(blob)
349
+ const link = document.createElement("a")
350
+ link.href = url
351
+
352
+ const contentDisposition = response.headers.get("Content-Disposition")
353
+ const filename = contentDisposition
354
+ ? contentDisposition.split("filename=")[1]?.replace(/"/g, "")
355
+ : `debiased_${hitlSession.filename}`
356
+
357
+ link.setAttribute("download", filename)
358
+ document.body.appendChild(link)
359
+ link.click()
360
+ link.remove()
361
+ window.URL.revokeObjectURL(url)
362
+
363
+ alert("PDF generated successfully!")
364
+ } catch (error) {
365
+ console.error("[Generate PDF Error]:", error)
366
+ alert(error instanceof Error ? error.message : "Failed to generate PDF")
367
+ } finally {
368
+ setIsGeneratingPDF(false)
369
+ }
370
+ }
371
+
372
+ const getPendingCount = () => {
373
+ if (!hitlSession) return 0
374
+ return hitlSession.sentences.filter(s => s.status === "pending" && s.is_biased).length
375
+ }
376
+
377
+ const getNeedsRegenerationCount = () => {
378
+ if (!hitlSession) return 0
379
+ return hitlSession.sentences.filter(s => s.status === "needs_regeneration").length
380
+ }
381
+
382
+ const isReadyForPDF = () => {
383
+ return getPendingCount() === 0 && getNeedsRegenerationCount() === 0
384
+ }
385
+
386
+ const calculateScore = (result: BiasAnalysisResult) => {
387
+ if (result.totalSentences === 0) return 100
388
+ return Math.round((result.unbiasedCount / result.totalSentences) * 100)
389
+ }
390
+
391
+ return (
392
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
393
+ {/* Input Side */}
394
+ <div className="space-y-6">
395
+ <Card className="border-primary/10 shadow-lg">
396
+ <CardHeader>
397
+ <CardTitle>Nepali Bias Checker</CardTitle>
398
+ <CardDescription>
399
+ Paste Nepali text or upload a PDF document to check for linguistic biases.
400
+ </CardDescription>
401
+ </CardHeader>
402
+ <CardContent className="space-y-6">
403
+ <div className="space-y-4">
404
+ <Textarea
405
+ placeholder="नेपाली पाठ यहाँ पेस्ट गर्नुहोस्... (Paste Nepali text here...)"
406
+ className="min-h-[300px] resize-none border-primary/10 focus-visible:ring-primary font-[system-ui]"
407
+ value={text}
408
+ onChange={(e) => {
409
+ setText(e.target.value)
410
+ if (e.target.value.trim()) setFile(null) // Clear file when typing
411
+ }}
412
+ disabled={!!file}
413
+ />
414
+ <div className="flex items-center gap-4">
415
+ <div className="h-px flex-1 bg-border" />
416
+ <span className="text-xs text-muted-foreground font-medium uppercase tracking-widest">or</span>
417
+ <div className="h-px flex-1 bg-border" />
418
+ </div>
419
+
420
+ {/* File Upload */}
421
+ <div className="relative">
422
+ <input
423
+ ref={fileInputRef}
424
+ type="file"
425
+ accept=".pdf"
426
+ onChange={handleFileChange}
427
+ className="hidden"
428
+ id="file-upload"
429
+ />
430
+ <label
431
+ htmlFor="file-upload"
432
+ onDragEnter={handleDrag}
433
+ onDragLeave={handleDrag}
434
+ onDragOver={handleDrag}
435
+ onDrop={handleDrop}
436
+ className={cn(
437
+ "flex flex-col items-center justify-center w-full h-40 border-2 border-dashed rounded-xl cursor-pointer transition-all duration-200",
438
+ dragActive
439
+ ? "border-primary bg-primary/5 scale-[1.01]"
440
+ : "border-muted hover:border-primary/50 hover:bg-muted/30",
441
+ file ? "border-success/50 bg-success/5" : ""
442
+ )}
443
+ >
444
+ {file ? (
445
+ <div className="flex flex-col items-center gap-2">
446
+ <CheckCircle2 className="h-10 w-10 text-success" />
447
+ <div className="flex items-center gap-2">
448
+ <FileText className="h-4 w-4" />
449
+ <p className="text-sm font-medium">{file.name}</p>
450
+ </div>
451
+ <Button
452
+ variant="ghost"
453
+ size="sm"
454
+ className="h-7 text-xs"
455
+ onClick={(e) => {
456
+ e.preventDefault()
457
+ removeFile()
458
+ }}
459
+ >
460
+ <X className="h-3 w-3 mr-1" /> Remove
461
+ </Button>
462
+ </div>
463
+ ) : (
464
+ <div className="flex flex-col items-center gap-2 p-6 text-center">
465
+ <Upload className="h-10 w-10 text-muted-foreground mb-2" />
466
+ <p className="text-sm font-semibold">Click or drag PDF to upload</p>
467
+ <p className="text-xs text-muted-foreground">Nepali PDF documents supported</p>
468
+ </div>
469
+ )}
470
+ </label>
471
+ </div>
472
+ </div>
473
+ </CardContent>
474
+ <CardContent className="pt-0">
475
+ <Button
476
+ className="w-full h-12 text-lg bg-primary hover:bg-primary/90"
477
+ onClick={analyzeText}
478
+ disabled={isAnalyzing || (!text.trim() && !file)}
479
+ >
480
+ {isAnalyzing ? (
481
+ <>
482
+ <Search className="mr-2 h-5 w-5 animate-spin" />
483
+ Analyzing Document...
484
+ </>
485
+ ) : (
486
+ <>
487
+ <ShieldAlert className="mr-2 h-5 w-5" />
488
+ Analyze for Bias
489
+ </>
490
+ )}
491
+ </Button>
492
+ </CardContent>
493
+ </Card>
494
+ </div>
495
+
496
+ {/* Results Side */}
497
+ <div className="space-y-6 sticky top-24">
498
+ {hitlSession ? (
499
+ <div className="space-y-6 animate-in fade-in slide-in-from-right-8 duration-500">
500
+ <Card className="border-primary/20 shadow-xl overflow-hidden">
501
+ <div className="h-2 w-full bg-primary" />
502
+ <CardHeader className="pb-2">
503
+ <div className="flex justify-between items-center mb-4">
504
+ <CardTitle>HITL Review Session</CardTitle>
505
+ <Badge variant="outline" className="text-lg py-1 px-3">
506
+ {hitlSession.biased_count} Biased
507
+ </Badge>
508
+ </div>
509
+ <Progress
510
+ value={((hitlSession.total_sentences - getPendingCount() - getNeedsRegenerationCount()) / hitlSession.total_sentences) * 100}
511
+ className="h-3"
512
+ />
513
+ <div className="flex justify-between text-xs text-muted-foreground font-bold pt-2">
514
+ <span>Pending: {getPendingCount()}</span>
515
+ <span>Needs Regen: {getNeedsRegenerationCount()}</span>
516
+ <span>Total: {hitlSession.total_sentences}</span>
517
+ </div>
518
+ </CardHeader>
519
+ <CardContent className="space-y-4 pt-4">
520
+ <div className="p-4 rounded-xl bg-muted/50 border border-primary/5">
521
+ <h4 className="flex items-center gap-2 text-sm font-bold mb-2">
522
+ <Info className="h-4 w-4 text-primary" />
523
+ Session Info
524
+ </h4>
525
+ <div className="space-y-2 text-sm">
526
+ <p>Session ID: <code className="text-xs bg-muted px-1 py-0.5 rounded">{hitlSession.session_id}</code></p>
527
+ <p>File: {hitlSession.filename}</p>
528
+ <p>Total Sentences: {hitlSession.total_sentences}</p>
529
+ <p className="text-destructive font-semibold">Biased: {hitlSession.biased_count}</p>
530
+ <p className="text-success font-semibold">Neutral: {hitlSession.neutral_count}</p>
531
+ </div>
532
+ </div>
533
+
534
+ {/* Biased Sentences Review */}
535
+ <div className="space-y-4">
536
+ <h4 className="text-sm font-bold flex items-center gap-2">
537
+ <AlertCircle className="h-4 w-4 text-destructive" />
538
+ Review Biased Sentences
539
+ </h4>
540
+ <div className="space-y-4 max-h-[500px] overflow-y-auto pr-2">
541
+ {hitlSession.sentences
542
+ .filter(s => s.is_biased)
543
+ .map((item) => (
544
+ <div key={item.sentence_id} className="p-4 rounded-xl border bg-background space-y-3 shadow-sm">
545
+ <div className="space-y-2">
546
+ <div className="flex items-center gap-2 flex-wrap mb-2">
547
+ <Badge variant="destructive" className="text-xs">
548
+ {categoryLabels[item.category] || item.category}
549
+ </Badge>
550
+ <Badge variant="outline" className="text-xs">
551
+ {Math.round(item.confidence * 100)}% confidence
552
+ </Badge>
553
+ <Badge
554
+ variant={item.status === "approved" ? "default" : item.status === "needs_regeneration" ? "destructive" : "secondary"}
555
+ className="text-xs"
556
+ >
557
+ {item.status}
558
+ </Badge>
559
+ </div>
560
+
561
+ {/* Original Sentence */}
562
+ <div>
563
+ <p className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-1">
564
+ Original (Biased):
565
+ </p>
566
+ <p className="text-sm font-medium leading-relaxed font-[system-ui] bg-destructive/5 p-2 rounded border border-destructive/20">
567
+ {item.original_sentence}
568
+ </p>
569
+ </div>
570
+
571
+ {/* Suggestion */}
572
+ {item.suggestion && (
573
+ <div>
574
+ <p className="text-xs font-bold text-success uppercase tracking-wider mb-1">
575
+ Suggested (Unbiased):
576
+ </p>
577
+ <p className="text-sm font-medium leading-relaxed font-[system-ui] bg-success/5 p-2 rounded border border-success/20">
578
+ {item.suggestion}
579
+ </p>
580
+ </div>
581
+ )}
582
+
583
+ {/* Action Buttons */}
584
+ {item.status === "pending" && item.suggestion && (
585
+ <div className="flex gap-2 pt-2">
586
+ <Button
587
+ size="sm"
588
+ variant="default"
589
+ className="flex-1"
590
+ onClick={() => handleApproveSuggestion(item.sentence_id, item.suggestion!)}
591
+ >
592
+ <ThumbsUp className="h-4 w-4 mr-1" />
593
+ Approve
594
+ </Button>
595
+ <Button
596
+ size="sm"
597
+ variant="destructive"
598
+ className="flex-1"
599
+ onClick={() => handleRejectSuggestion(item.sentence_id)}
600
+ >
601
+ <ThumbsDown className="h-4 w-4 mr-1" />
602
+ Reject
603
+ </Button>
604
+ </div>
605
+ )}
606
+
607
+ {item.status === "needs_regeneration" && (
608
+ <div className="pt-2">
609
+ <Button
610
+ size="sm"
611
+ variant="outline"
612
+ className="w-full"
613
+ onClick={() => handleRegenerateSuggestion(item.sentence_id)}
614
+ >
615
+ <RefreshCw className="h-4 w-4 mr-1" />
616
+ Regenerate Suggestion
617
+ </Button>
618
+ </div>
619
+ )}
620
+
621
+ {item.status === "approved" && (
622
+ <div className="pt-2">
623
+ <div className="flex items-center gap-2 text-success text-sm">
624
+ <CheckCircle2 className="h-4 w-4" />
625
+ <span className="font-semibold">Approved</span>
626
+ </div>
627
+ {item.approved_suggestion && (
628
+ <p className="text-xs text-muted-foreground mt-1">
629
+ Approved: {item.approved_suggestion}
630
+ </p>
631
+ )}
632
+ </div>
633
+ )}
634
+ </div>
635
+ </div>
636
+ ))}
637
+ </div>
638
+ </div>
639
+
640
+ {/* Generate PDF Button */}
641
+ <div className="pt-4">
642
+ <Button
643
+ className="w-full h-12 text-lg"
644
+ onClick={handleGeneratePDF}
645
+ disabled={!isReadyForPDF() || isGeneratingPDF}
646
+ >
647
+ {isGeneratingPDF ? (
648
+ <>
649
+ <RefreshCw className="mr-2 h-5 w-5 animate-spin" />
650
+ Generating PDF...
651
+ </>
652
+ ) : !isReadyForPDF() ? (
653
+ <>
654
+ <AlertCircle className="mr-2 h-5 w-5" />
655
+ Review All Sentences First
656
+ </>
657
+ ) : (
658
+ <>
659
+ <Download className="mr-2 h-5 w-5" />
660
+ Generate Debiased PDF
661
+ </>
662
+ )}
663
+ </Button>
664
+ {!isReadyForPDF() && (
665
+ <p className="text-xs text-center text-muted-foreground mt-2">
666
+ {getPendingCount()} pending, {getNeedsRegenerationCount()} need regeneration
667
+ </p>
668
+ )}
669
+ </div>
670
+ </CardContent>
671
+ </Card>
672
+ </div>
673
+ ) : result ? (
674
+ <div className="space-y-6 animate-in fade-in slide-in-from-right-8 duration-500">
675
+ <Card className="border-primary/20 shadow-xl overflow-hidden">
676
+ <div
677
+ className={cn(
678
+ "h-2 w-full",
679
+ calculateScore(result) > 70
680
+ ? "bg-success"
681
+ : calculateScore(result) > 40
682
+ ? "bg-accent"
683
+ : "bg-destructive"
684
+ )}
685
+ />
686
+ <CardHeader className="pb-2">
687
+ <div className="flex justify-between items-center mb-4">
688
+ <CardTitle>Analysis Results</CardTitle>
689
+ <Badge variant="outline" className="text-lg py-1 px-3">
690
+ Score: {calculateScore(result)}/100
691
+ </Badge>
692
+ </div>
693
+ <Progress value={calculateScore(result)} className="h-3" />
694
+ <div className="flex justify-between text-[10px] text-muted-foreground font-bold pt-2 uppercase tracking-tighter">
695
+ <span>Critical Bias</span>
696
+ <span>Moderate</span>
697
+ <span>Inclusive</span>
698
+ </div>
699
+ </CardHeader>
700
+ <CardContent className="space-y-4 pt-4">
701
+ <div className="p-4 rounded-xl bg-muted/50 border border-primary/5">
702
+ <h4 className="flex items-center gap-2 text-sm font-bold mb-2">
703
+ <Info className="h-4 w-4 text-primary" />
704
+ Summary
705
+ </h4>
706
+ <div className="space-y-2">
707
+ <p className="text-base leading-relaxed font-semibold">
708
+ Biased sentences = <span className="text-destructive">{result.biasedCount}</span>
709
+ </p>
710
+ <p className="text-base leading-relaxed font-semibold">
711
+ Unbiased sentences = <span className="text-success">{result.unbiasedCount}</span>
712
+ </p>
713
+ {result.filename && (
714
+ <p className="text-xs text-muted-foreground mt-2">File: {result.filename}</p>
715
+ )}
716
+ </div>
717
+ </div>
718
+
719
+ {result.biasedSentences.length > 0 && (
720
+ <div className="space-y-4">
721
+ <h4 className="text-sm font-bold flex items-center gap-2">
722
+ <AlertCircle className="h-4 w-4 text-destructive" />
723
+ Biased Sentences with Suggestions
724
+ </h4>
725
+ <div className="space-y-4 max-h-[600px] overflow-y-auto pr-2">
726
+ {result.biasedSentences.map((item, idx) => (
727
+ <div key={idx} className="p-4 rounded-xl border bg-background space-y-3 shadow-sm">
728
+ <div className="space-y-2">
729
+ <div className="flex items-center gap-2 flex-wrap mb-2">
730
+ <Badge variant="destructive" className="text-xs">
731
+ {categoryLabels[item.category] || item.category}
732
+ </Badge>
733
+ <Badge variant="outline" className="text-xs">
734
+ {Math.round(item.confidence * 100)}% confidence
735
+ </Badge>
736
+ </div>
737
+ <div>
738
+ <p className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-1">
739
+ Original (Biased):
740
+ </p>
741
+ <p className="text-sm font-medium leading-relaxed font-[system-ui] bg-destructive/5 p-2 rounded border border-destructive/20">
742
+ {item.original}
743
+ </p>
744
+ </div>
745
+ {item.explanation && (
746
+ <div>
747
+ <p className="text-xs font-bold text-amber-600 uppercase tracking-wider mb-1">
748
+ Why is this biased?
749
+ </p>
750
+ <p className="text-xs leading-relaxed bg-amber-50 dark:bg-amber-950/20 p-2 rounded border border-amber-200 dark:border-amber-800 text-amber-900 dark:text-amber-100">
751
+ {item.explanation}
752
+ </p>
753
+ </div>
754
+ )}
755
+ {item.suggestion && item.success ? (
756
+ <div>
757
+ <p className="text-xs font-bold text-success uppercase tracking-wider mb-1">
758
+ Suggested (Unbiased):
759
+ </p>
760
+ <p className="text-sm font-medium leading-relaxed font-[system-ui] bg-success/5 p-2 rounded border border-success/20">
761
+ {item.suggestion}
762
+ </p>
763
+ </div>
764
+ ) : (
765
+ <div>
766
+ <p className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-1">
767
+ Suggestion:
768
+ </p>
769
+ <p className="text-xs text-muted-foreground italic">
770
+ Suggestion not available
771
+ </p>
772
+ </div>
773
+ )}
774
+ </div>
775
+ </div>
776
+ ))}
777
+ </div>
778
+ </div>
779
+ )}
780
+
781
+ {result.biasedSentences.length === 0 && (
782
+ <div className="p-6 rounded-xl border border-success/20 bg-success/5 text-center">
783
+ <CheckCircle2 className="h-12 w-12 text-success mx-auto mb-2" />
784
+ <p className="font-semibold text-success">No biases detected!</p>
785
+ <p className="text-xs text-muted-foreground mt-1">The text appears to be neutral and inclusive.</p>
786
+ </div>
787
+ )}
788
+ </CardContent>
789
+ </Card>
790
+ </div>
791
+ ) : (
792
+ <div className="h-full min-h-[500px] flex flex-col items-center justify-center text-center p-8 rounded-3xl border-2 border-dashed bg-muted/10 opacity-60">
793
+ <div className="h-20 w-20 rounded-full bg-muted flex items-center justify-center mb-6">
794
+ <Search className="h-10 w-10 text-muted-foreground/50" />
795
+ </div>
796
+ <h3 className="text-xl font-bold mb-2 text-muted-foreground">Waiting for analysis</h3>
797
+ <p className="max-w-[300px] text-sm text-muted-foreground">
798
+ Enter Nepali text or upload a PDF on the left and click "Analyze for Bias" to see linguistic insights.
799
+ </p>
800
+ </div>
801
+ )}
802
+ </div>
803
+ </div>
804
+ )
805
+ }
Frontend/components/chatbot/bias-checker.tsx.backup ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Textarea } from "@/components/ui/textarea"
6
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
7
+ import { Progress } from "@/components/ui/progress"
8
+ import { Badge } from "@/components/ui/badge"
9
+ import { FileUpload } from "@/components/common/file-upload"
10
+ import { AlertCircle, Info, Search, History, ArrowRight, ShieldAlert } from "lucide-react"
11
+ import { cn } from "@/lib/utils"
12
+
13
+ interface BiasResult {
14
+ score: number
15
+ problematicPhrases: { phrase: string; reason: string; recommendation: string }[]
16
+ summary: string
17
+ }
18
+
19
+ export function BiasChecker() {
20
+ const [text, setText] = useState("")
21
+ const [isAnalyzing, setIsAnalyzing] = useState(false)
22
+ const [result, setResult] = useState<BiasResult | null>(null)
23
+
24
+ const analyzeText = () => {
25
+ if (!text.trim()) return
26
+ setIsAnalyzing(true)
27
+
28
+ // Simulate AI analysis delay
29
+ setTimeout(() => {
30
+ const mockResult: BiasResult = {
31
+ score: 65,
32
+ summary:
33
+ "The document contains several instances of exclusionary language and ambiguous legal terminology that might favor certain socioeconomic groups.",
34
+ problematicPhrases: [
35
+ {
36
+ phrase: "duly qualified applicants",
37
+ reason: "Vague terminology that often serves as a gatekeeping mechanism without specific criteria.",
38
+ recommendation: "Use 'applicants meeting the specific criteria listed below'.",
39
+ },
40
+ {
41
+ phrase: "standard processing fees apply",
42
+ reason: "Potentially exclusionary for lower-income individuals without stating exact amounts.",
43
+ recommendation: "Clearly list the fee structure or mention available waivers.",
44
+ },
45
+ {
46
+ phrase: "formal education only",
47
+ reason: "Excludes qualified individuals with non-traditional or vocational backgrounds.",
48
+ recommendation: "Consider 'equivalent experience' or specific skill-based testing.",
49
+ },
50
+ ],
51
+ }
52
+ setResult(mockResult)
53
+ setIsAnalyzing(false)
54
+ }, 2000)
55
+ }
56
+
57
+ return (
58
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
59
+ {/* Input Side */}
60
+ <div className="space-y-6">
61
+ <Card className="border-primary/10 shadow-lg">
62
+ <CardHeader>
63
+ <CardTitle>Legal Notice Analyzer</CardTitle>
64
+ <CardDescription>Paste legal content or upload a document to check for linguistic biases.</CardDescription>
65
+ </CardHeader>
66
+ <CardContent className="space-y-6">
67
+ <div className="space-y-4">
68
+ <Textarea
69
+ placeholder="Paste legal notice content here..."
70
+ className="min-h-[300px] resize-none border-primary/10 focus-visible:ring-primary"
71
+ value={text}
72
+ onChange={(e) => setText(e.target.value)}
73
+ />
74
+ <div className="flex items-center gap-4">
75
+ <div className="h-px flex-1 bg-border" />
76
+ <span className="text-xs text-muted-foreground font-medium uppercase tracking-widest">or</span>
77
+ <div className="h-px flex-1 bg-border" />
78
+ </div>
79
+ <FileUpload onFileSelect={(txt) => setText(txt)} />
80
+ </div>
81
+ </CardContent>
82
+ <CardContent className="pt-0">
83
+ <Button
84
+ className="w-full h-12 text-lg bg-primary hover:bg-primary/90"
85
+ onClick={analyzeText}
86
+ disabled={isAnalyzing || !text.trim()}
87
+ >
88
+ {isAnalyzing ? (
89
+ <>
90
+ <Search className="mr-2 h-5 w-5 animate-spin" />
91
+ Analyzing Document...
92
+ </>
93
+ ) : (
94
+ <>
95
+ <ShieldAlert className="mr-2 h-5 w-5" />
96
+ Analyze for Bias
97
+ </>
98
+ )}
99
+ </Button>
100
+ </CardContent>
101
+ </Card>
102
+
103
+ <Card className="bg-muted/50 border-none">
104
+ <CardHeader className="py-4">
105
+ <div className="flex items-center gap-2 text-sm font-semibold">
106
+ <History className="h-4 w-4 text-primary" />
107
+ Recent Analyses
108
+ </div>
109
+ </CardHeader>
110
+ <CardContent className="py-0 pb-4">
111
+ <div className="space-y-2">
112
+ {[1, 2].map((i) => (
113
+ <div
114
+ key={i}
115
+ className="flex items-center justify-between p-3 rounded-lg bg-background border text-sm group cursor-pointer hover:border-primary/30 transition-colors"
116
+ >
117
+ <span className="truncate max-w-[200px] font-medium">Labor Notice Draft 0{i}.txt</span>
118
+ <div className="flex items-center gap-3">
119
+ <Badge variant="secondary" className="bg-success/10 text-success border-success/20">
120
+ Clean
121
+ </Badge>
122
+ <ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-transform group-hover:translate-x-1" />
123
+ </div>
124
+ </div>
125
+ ))}
126
+ </div>
127
+ </CardContent>
128
+ </Card>
129
+ </div>
130
+
131
+ {/* Results Side */}
132
+ <div className="space-y-6 sticky top-24">
133
+ {result ? (
134
+ <div className="space-y-6 animate-in fade-in slide-in-from-right-8 duration-500">
135
+ <Card className="border-primary/20 shadow-xl overflow-hidden">
136
+ <div
137
+ className={cn(
138
+ "h-2 w-full",
139
+ result.score > 70 ? "bg-success" : result.score > 40 ? "bg-accent" : "bg-destructive",
140
+ )}
141
+ />
142
+ <CardHeader className="pb-2">
143
+ <div className="flex justify-between items-center mb-4">
144
+ <CardTitle>Analysis Results</CardTitle>
145
+ <Badge variant="outline" className="text-lg py-1 px-3">
146
+ Score: {result.score}/100
147
+ </Badge>
148
+ </div>
149
+ <Progress value={result.score} className="h-3" />
150
+ <div className="flex justify-between text-[10px] text-muted-foreground font-bold pt-2 uppercase tracking-tighter">
151
+ <span>Critical Bias</span>
152
+ <span>Moderate</span>
153
+ <span>Inclusive</span>
154
+ </div>
155
+ </CardHeader>
156
+ <CardContent className="space-y-4 pt-4">
157
+ <div className="p-4 rounded-xl bg-muted/50 border border-primary/5">
158
+ <h4 className="flex items-center gap-2 text-sm font-bold mb-2">
159
+ <Info className="h-4 w-4 text-primary" />
160
+ Summary
161
+ </h4>
162
+ <p className="text-sm leading-relaxed">{result.summary}</p>
163
+ </div>
164
+
165
+ <div className="space-y-4">
166
+ <h4 className="text-sm font-bold flex items-center gap-2">
167
+ <AlertCircle className="h-4 w-4 text-destructive" />
168
+ Detected Issues
169
+ </h4>
170
+ <div className="space-y-3">
171
+ {result.problematicPhrases.map((item, idx) => (
172
+ <div key={idx} className="p-4 rounded-xl border bg-background space-y-3 shadow-sm">
173
+ <div className="flex items-start justify-between">
174
+ <code className="text-destructive font-bold bg-destructive/5 px-2 py-0.5 rounded text-sm">
175
+ "{item.phrase}"
176
+ </code>
177
+ </div>
178
+ <div className="grid gap-2">
179
+ <p className="text-xs leading-relaxed">
180
+ <span className="font-bold text-muted-foreground uppercase tracking-widest text-[9px] mr-2">
181
+ Reason:
182
+ </span>
183
+ {item.reason}
184
+ </p>
185
+ <div className="p-2 rounded bg-success/5 border border-success/10">
186
+ <p className="text-xs leading-relaxed font-medium text-success">
187
+ <span className="font-bold uppercase tracking-widest text-[9px] mr-2">Recommended:</span>
188
+ {item.recommendation}
189
+ </p>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ ))}
194
+ </div>
195
+ </div>
196
+ </CardContent>
197
+ </Card>
198
+ </div>
199
+ ) : (
200
+ <div className="h-full min-h-[500px] flex flex-col items-center justify-center text-center p-8 rounded-3xl border-2 border-dashed bg-muted/10 opacity-60">
201
+ <div className="h-20 w-20 rounded-full bg-muted flex items-center justify-center mb-6">
202
+ <Search className="h-10 w-10 text-muted-foreground/50" />
203
+ </div>
204
+ <h3 className="text-xl font-bold mb-2 text-muted-foreground">Waiting for analysis</h3>
205
+ <p className="max-w-[300px] text-sm text-muted-foreground">
206
+ Enter text on the left and click "Analyze for Bias" to see linguistic insights and recommendations.
207
+ </p>
208
+ </div>
209
+ )}
210
+ </div>
211
+ </div>
212
+ )
213
+ }
Frontend/components/chatbot/law-chatbot.tsx ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useRef, useEffect } from "react"
4
+ import { useRouter } from "next/navigation"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Input } from "@/components/ui/input"
7
+ import { ScrollArea } from "@/components/ui/scroll-area"
8
+ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
9
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
10
+ import { Badge } from "@/components/ui/badge"
11
+ import { Send, Scale, User, Download, Plus, MessageSquare, Trash2, Menu, X } from "lucide-react"
12
+ import { cn } from "@/lib/utils"
13
+ import ReactMarkdown from "react-markdown"
14
+ import remarkGfm from "remark-gfm"
15
+ import { useToast } from "@/hooks/use-toast"
16
+
17
+ interface SuggestedAction {
18
+ action: string
19
+ description: string
20
+ letter_type: string
21
+ prompt: string
22
+ }
23
+
24
+ interface Message {
25
+ id: string
26
+ role: "user" | "assistant"
27
+ content: string
28
+ timestamp: Date
29
+ suggested_action?: SuggestedAction
30
+ metadata?: {
31
+ summary?: string
32
+ key_point?: string
33
+ next_steps?: string
34
+ sources?: any[]
35
+ context_used?: boolean
36
+ is_non_legal?: boolean
37
+ }
38
+ }
39
+
40
+ interface Conversation {
41
+ id: string
42
+ title: string
43
+ updated_at: string
44
+ message_count: number
45
+ }
46
+
47
+ export function LawChatbot() {
48
+ const router = useRouter()
49
+ const [messages, setMessages] = useState<Message[]>([
50
+ {
51
+ id: "1",
52
+ role: "assistant",
53
+ content: "Namaste! I am your Nepali Law Assistant. How can I help you today?",
54
+ timestamp: new Date(),
55
+ },
56
+ ])
57
+ const [input, setInput] = useState("")
58
+ const [isTyping, setIsTyping] = useState(false)
59
+ const [conversationId, setConversationId] = useState<string | null>(null)
60
+ const [conversations, setConversations] = useState<Conversation[]>([])
61
+ const [isLoadingConversations, setIsLoadingConversations] = useState(false)
62
+ const [showLetterDialog, setShowLetterDialog] = useState(false)
63
+ const [pendingAction, setPendingAction] = useState<SuggestedAction | null>(null)
64
+ const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false)
65
+ const scrollAreaRef = useRef<HTMLDivElement>(null)
66
+ const { toast } = useToast()
67
+
68
+ const suggestedQuestions = [
69
+ "What are my property rights?",
70
+ "Labor law basics",
71
+ "Consumer rights",
72
+ "Women's rights in Nepal",
73
+ ]
74
+
75
+ // Load conversations on mount
76
+ useEffect(() => {
77
+ loadConversations()
78
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
+ }, [])
80
+
81
+ // Scroll to bottom whenever messages change
82
+ useEffect(() => {
83
+ const timer = setTimeout(() => {
84
+ if (scrollAreaRef.current) {
85
+ const scrollContainer = scrollAreaRef.current.querySelector("[data-radix-scroll-area-viewport]")
86
+ if (scrollContainer) {
87
+ scrollContainer.scrollTo({
88
+ top: scrollContainer.scrollHeight,
89
+ behavior: "smooth",
90
+ })
91
+ }
92
+ }
93
+ }, 100)
94
+ return () => clearTimeout(timer)
95
+ }, [messages, isTyping])
96
+
97
+ const getAuthHeaders = () => {
98
+ const token = localStorage.getItem("access_token")
99
+ return {
100
+ "Content-Type": "application/json",
101
+ ...(token && { Authorization: `Bearer ${token}` }),
102
+ }
103
+ }
104
+
105
+ // Use public env var so frontend can target backend in different environments
106
+ const BACKEND_URL = (process.env.NEXT_PUBLIC_BACKEND_URL as string) || "http://localhost:8000"
107
+
108
+ const loadConversations = async () => {
109
+ const token = localStorage.getItem("access_token")
110
+ if (!token) return
111
+
112
+ setIsLoadingConversations(true)
113
+ try {
114
+ const response = await fetch(`${BACKEND_URL}/api/v1/chat-history/conversations`, {
115
+ headers: getAuthHeaders(),
116
+ })
117
+
118
+ if (response.ok) {
119
+ const data = await response.json()
120
+ setConversations(data)
121
+ }
122
+ } catch (error) {
123
+ console.error("Failed to load conversations:", error)
124
+ } finally {
125
+ setIsLoadingConversations(false)
126
+ }
127
+ }
128
+
129
+ const loadConversation = async (convId: string) => {
130
+ try {
131
+ const response = await fetch(`${BACKEND_URL}/api/v1/chat-history/conversations/${convId}`, {
132
+ headers: getAuthHeaders(),
133
+ })
134
+
135
+ if (response.ok) {
136
+ const data = await response.json()
137
+ const loadedMessages: Message[] = data.messages.map((msg: any) => {
138
+ let content = msg.content
139
+
140
+ // If this is an assistant message with metadata, reconstruct the full formatted content
141
+ if (msg.role === "assistant" && msg.metadata) {
142
+ const meta = msg.metadata
143
+
144
+ // Check if metadata has the structured fields
145
+ if (meta.summary || meta.key_point || meta.next_steps) {
146
+ // Format sources
147
+ let sourcesText = ""
148
+ if (meta.sources && meta.sources.length > 0) {
149
+ const formattedSources = meta.sources
150
+ .filter((s: any) => s && (s.file || s.section))
151
+ .map((s: any, i: number) => {
152
+ let fileName = "Legal Document"
153
+ if (s.file) {
154
+ fileName = s.file
155
+ .replace(/_en\.pdf$/i, "")
156
+ .replace(/\.pdf$/i, "")
157
+ .replace(/_/g, " ")
158
+ }
159
+ const section = s.section || s.article_section || "General Reference"
160
+ return ` ${i + 1}. **${fileName}** (${section})`
161
+ })
162
+ .join("\n")
163
+
164
+ if (formattedSources) {
165
+ sourcesText = `\n\n### 📚 Resources\n${formattedSources}`
166
+ }
167
+ }
168
+
169
+ const contextBadge = meta.context_used ? "\n\n> 💡 *Used conversation context*" : ""
170
+
171
+ // Reconstruct the full formatted content
172
+ content = `### 📝 Summary\n${meta.summary || ""}
173
+
174
+ ### 💬 Detailed Explanation
175
+ ${msg.content}
176
+
177
+ ### 🔑 Key Points
178
+ - ${meta.key_point || ""}
179
+
180
+ ### 📋 Next Steps
181
+ ${meta.next_steps || ""}${sourcesText}${contextBadge}`.trim()
182
+ }
183
+ }
184
+
185
+ return {
186
+ id: msg.id,
187
+ role: msg.role,
188
+ content: content,
189
+ timestamp: new Date(msg.timestamp),
190
+ metadata: msg.metadata,
191
+ }
192
+ })
193
+ setMessages(loadedMessages)
194
+ setConversationId(convId)
195
+ // Close mobile sidebar after loading conversation
196
+ setIsMobileSidebarOpen(false)
197
+ }
198
+ } catch (error) {
199
+ console.error("Failed to load conversation:", error)
200
+ toast({
201
+ title: "Error",
202
+ description: "Failed to load conversation",
203
+ variant: "destructive",
204
+ })
205
+ }
206
+ }
207
+
208
+ const createNewConversation = async (firstMessage: string) => {
209
+ const token = localStorage.getItem("access_token")
210
+ if (!token) {
211
+ toast({
212
+ title: "Authentication Required",
213
+ description: "Please login to save chat history",
214
+ variant: "destructive",
215
+ })
216
+ return null
217
+ }
218
+
219
+ try {
220
+ const title = firstMessage.slice(0, 50) + (firstMessage.length > 50 ? "..." : "")
221
+ const response = await fetch(`${BACKEND_URL}/api/v1/chat-history/conversations`, {
222
+ method: "POST",
223
+ headers: getAuthHeaders(),
224
+ body: JSON.stringify({ title }),
225
+ })
226
+
227
+ if (response.ok) {
228
+ const data = await response.json()
229
+ setConversationId(data.id)
230
+ loadConversations() // Refresh conversation list
231
+ return data.id
232
+ }
233
+ } catch (error) {
234
+ console.error("Failed to create conversation:", error)
235
+ }
236
+ return null
237
+ }
238
+
239
+ const deleteConversation = async (convId: string) => {
240
+ try {
241
+ const response = await fetch(`${BACKEND_URL}/api/v1/chat-history/conversations/${convId}`, {
242
+ method: "DELETE",
243
+ headers: getAuthHeaders(),
244
+ })
245
+
246
+ if (response.ok) {
247
+ toast({
248
+ title: "Success",
249
+ description: "Conversation deleted successfully",
250
+ })
251
+
252
+ // If deleted conversation was active, start new chat
253
+ if (conversationId === convId) {
254
+ startNewChat()
255
+ }
256
+
257
+ // Refresh conversation list
258
+ loadConversations()
259
+ } else {
260
+ throw new Error("Failed to delete")
261
+ }
262
+ } catch (error) {
263
+ console.error("Failed to delete conversation:", error)
264
+ toast({
265
+ title: "Error",
266
+ description: "Failed to delete conversation",
267
+ variant: "destructive",
268
+ })
269
+ }
270
+ }
271
+
272
+
273
+ const confirmLetterGeneration = () => {
274
+ if (!pendingAction) return
275
+
276
+ // Store the description in localStorage to pass to letter generator page
277
+ localStorage.setItem("letter_generation_prompt", pendingAction.description)
278
+ localStorage.setItem("letter_type", pendingAction.letter_type)
279
+
280
+ // Close dialog
281
+ setShowLetterDialog(false)
282
+ setPendingAction(null)
283
+
284
+ // Redirect to letter generator page
285
+ router.push("/letter-generator")
286
+
287
+ toast({
288
+ title: "Redirecting...",
289
+ description: "Taking you to the letter generation section",
290
+ })
291
+ }
292
+
293
+ const cancelLetterGeneration = () => {
294
+ setShowLetterDialog(false)
295
+ setPendingAction(null)
296
+ }
297
+
298
+ const startNewChat = () => {
299
+ setMessages([
300
+ {
301
+ id: "1",
302
+ role: "assistant",
303
+ content: "Namaste! I am your Nepali Law Assistant. How can I help you today?",
304
+ timestamp: new Date(),
305
+ },
306
+ ])
307
+ setConversationId(null)
308
+ // Close mobile sidebar after starting new chat
309
+ setIsMobileSidebarOpen(false)
310
+ }
311
+
312
+ const handleSend = async (content: string) => {
313
+ if (!content.trim()) return
314
+
315
+ const userMsg: Message = {
316
+ id: Date.now().toString(),
317
+ role: "user",
318
+ content,
319
+ timestamp: new Date(),
320
+ }
321
+
322
+ setMessages((prev) => [...prev, userMsg])
323
+ setInput("")
324
+ setIsTyping(true)
325
+
326
+ try {
327
+ // Create conversation if this is the first message
328
+ let currentConvId = conversationId
329
+ if (!currentConvId && localStorage.getItem("access_token")) {
330
+ currentConvId = await createNewConversation(content)
331
+ }
332
+
333
+ const token = localStorage.getItem("access_token")
334
+ const headers: Record<string, string> = { "Content-Type": "application/json" }
335
+ if (token) {
336
+ headers["Authorization"] = `Bearer ${token}`
337
+ }
338
+
339
+ const response = await fetch("/api/Legal_Chat", {
340
+ method: "POST",
341
+ headers,
342
+ body: JSON.stringify({
343
+ messages: [...messages, { role: "user", content }],
344
+ type: "legal",
345
+ conversation_id: currentConvId,
346
+ }),
347
+ })
348
+
349
+ const data = await response.json()
350
+
351
+ const assistantMsg: Message = {
352
+ id: (Date.now() + 1).toString(),
353
+ role: "assistant",
354
+ content: data.content || data.error || "I am having trouble connecting to my legal database.",
355
+ timestamp: new Date(),
356
+ suggested_action: data.suggested_action || undefined,
357
+ }
358
+ setMessages((prev) => [...prev, assistantMsg])
359
+
360
+ // If there's a suggested action, show the dialog
361
+ if (data.suggested_action) {
362
+ setPendingAction(data.suggested_action)
363
+ setShowLetterDialog(true)
364
+ }
365
+
366
+ // Note: Messages are now automatically saved by the backend /chat endpoint
367
+ } catch (error) {
368
+ console.error("[Legal Chat Error]:", error)
369
+ const errorMsg: Message = {
370
+ id: (Date.now() + 1).toString(),
371
+ role: "assistant",
372
+ content: "I encountered an error while processing your request. Please ensure the backend server is running.",
373
+ timestamp: new Date(),
374
+ }
375
+ setMessages((prev) => [...prev, errorMsg])
376
+ } finally {
377
+ setIsTyping(false)
378
+ }
379
+ }
380
+
381
+ const exportChat = () => {
382
+ const chatContent = messages
383
+ .map((m) => `[${m.role.toUpperCase()} - ${m.timestamp.toLocaleTimeString()}]: ${m.content}`)
384
+ .join("\n\n")
385
+ const blob = new Blob([chatContent], { type: "text/plain" })
386
+ const url = URL.createObjectURL(blob)
387
+ const a = document.createElement("a")
388
+ a.href = url
389
+ a.download = "legal-chat-history.txt"
390
+ a.click()
391
+ }
392
+
393
+ return (
394
+ <div className="w-full max-w-[1600px] mx-auto">
395
+ <div className="flex h-[calc(100vh-10rem)] gap-4 relative">
396
+ {/* Mobile Sidebar Overlay */}
397
+ {isMobileSidebarOpen && (
398
+ <div
399
+ className="fixed inset-0 bg-black/50 z-40 lg:hidden"
400
+ onClick={() => setIsMobileSidebarOpen(false)}
401
+ />
402
+ )}
403
+
404
+ {/* Sidebar - Chat History */}
405
+ <div
406
+ className={cn(
407
+ "w-64 flex-col gap-4 border-r pr-4 bg-background z-50",
408
+ "lg:flex lg:relative lg:translate-x-0",
409
+ "fixed left-0 top-0 h-full transition-transform duration-300 ease-in-out p-4 lg:p-0",
410
+ isMobileSidebarOpen ? "flex translate-x-0" : "hidden lg:flex -translate-x-full lg:translate-x-0"
411
+ )}
412
+ >
413
+ {/* Close button for mobile */}
414
+ <div className="flex items-center justify-between lg:hidden mb-4">
415
+ <h3 className="font-semibold">Chat History</h3>
416
+ <Button
417
+ variant="ghost"
418
+ size="icon"
419
+ onClick={() => setIsMobileSidebarOpen(false)}
420
+ className="h-8 w-8"
421
+ >
422
+ <X className="h-4 w-4" />
423
+ </Button>
424
+ </div>
425
+
426
+ <Button className="w-full justify-start gap-2 bg-primary" onClick={startNewChat}>
427
+ <Plus className="h-4 w-4" /> New Conversation
428
+ </Button>
429
+ <ScrollArea className="flex-1">
430
+ <div className="space-y-2">
431
+ <p className="text-xs font-semibold text-muted-foreground px-2 uppercase tracking-wider">Recent Chats</p>
432
+ {isLoadingConversations ? (
433
+ <p className="text-xs text-muted-foreground px-2">Loading...</p>
434
+ ) : conversations.length > 0 ? (
435
+ conversations.map((conv) => (
436
+ <div
437
+ key={conv.id}
438
+ className={cn(
439
+ "w-full rounded-lg transition-colors border flex items-center gap-2",
440
+ conversationId === conv.id
441
+ ? "bg-muted border-border"
442
+ : "border-transparent hover:border-border hover:bg-muted/50"
443
+ )}
444
+ >
445
+ <button
446
+ className="p-2 rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-all shrink-0"
447
+ onClick={(e) => {
448
+ e.stopPropagation()
449
+ if (confirm("Delete this conversation? This cannot be undone.")) {
450
+ deleteConversation(conv.id)
451
+ }
452
+ }}
453
+ title="Delete conversation"
454
+ >
455
+ <Trash2 className="h-4 w-4" />
456
+ </button>
457
+ <button
458
+ className="flex-1 text-left p-3 pr-2"
459
+ onClick={() => loadConversation(conv.id)}
460
+ >
461
+ <div className="flex items-center gap-2 mb-1">
462
+ <MessageSquare className="h-4 w-4 text-primary shrink-0" />
463
+ <span className="text-sm font-medium truncate">{conv.title}</span>
464
+ </div>
465
+ <div className="flex items-center justify-between">
466
+ <span className="text-xs text-muted-foreground">
467
+ {new Date(conv.updated_at).toLocaleDateString()}
468
+ </span>
469
+ <span className="text-xs text-muted-foreground">{conv.message_count} msgs</span>
470
+ </div>
471
+ </button>
472
+ </div>
473
+ ))
474
+ ) : (
475
+ <p className="text-xs text-muted-foreground px-2">No conversations yet</p>
476
+ )}
477
+ </div>
478
+ </ScrollArea>
479
+ </div>
480
+
481
+ {/* Main Chat Interface */}
482
+ <Card className="flex-1 flex flex-col shadow-lg border-primary/10 overflow-hidden bg-card/50 backdrop-blur-sm">
483
+ <CardHeader className="border-b py-4 flex flex-row items-center justify-between">
484
+ <div className="flex items-center gap-3">
485
+ {/* Mobile menu button */}
486
+ <Button
487
+ variant="ghost"
488
+ size="icon"
489
+ className="lg:hidden"
490
+ onClick={() => setIsMobileSidebarOpen(true)}
491
+ title="Open chat history"
492
+ >
493
+ <Menu className="h-5 w-5" />
494
+ </Button>
495
+
496
+ <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
497
+ <Scale className="h-5 w-5" />
498
+ </div>
499
+ <div>
500
+ <CardTitle className="text-lg">Nepali Law Assistant</CardTitle>
501
+ <div className="flex items-center gap-2">
502
+ <span className="h-2 w-2 rounded-full bg-success animate-pulse" />
503
+ <span className="text-xs text-muted-foreground">AI Powered & Ready</span>
504
+ </div>
505
+ </div>
506
+ </div>
507
+ <div className="flex items-center gap-2">
508
+ <Button variant="ghost" size="sm" onClick={exportChat} title="Export conversation">
509
+ <Download className="h-4 w-4 mr-2" />
510
+ Export
511
+ </Button>
512
+ </div>
513
+ </CardHeader>
514
+
515
+ <CardContent className="flex-1 p-0 overflow-hidden">
516
+ <ScrollArea ref={scrollAreaRef} className="h-full">
517
+ <div className="space-y-6 max-w-4xl mx-auto p-4 md:p-6">
518
+ {messages.map((m) => (
519
+ <div
520
+ key={m.id}
521
+ className={cn(
522
+ "flex gap-3 animate-in fade-in slide-in-from-bottom-2",
523
+ m.role === "user" ? "flex-row-reverse" : "flex-row",
524
+ )}
525
+ >
526
+ <Avatar className={cn("h-8 w-8 border", m.role === "assistant" ? "bg-primary/10" : "bg-muted")}>
527
+ <AvatarFallback className={m.role === "assistant" ? "text-primary" : "text-muted-foreground"}>
528
+ {m.role === "assistant" ? <Scale className="h-4 w-4" /> : <User className="h-4 w-4" />}
529
+ </AvatarFallback>
530
+ </Avatar>
531
+ <div
532
+ className={cn("flex flex-col gap-1 max-w-[80%]", m.role === "user" ? "items-end" : "items-start")}
533
+ >
534
+ <div
535
+ className={cn(
536
+ "rounded-2xl px-4 py-3 text-sm shadow-sm",
537
+ m.role === "user"
538
+ ? "bg-primary text-primary-foreground rounded-tr-none"
539
+ : "bg-muted/80 backdrop-blur-sm rounded-tl-none border",
540
+ )}
541
+ >
542
+ {m.role === "assistant" ? (
543
+ <div className="prose prose-sm dark:prose-invert max-w-none overflow-wrap-anywhere wrap-break-word">
544
+ <ReactMarkdown
545
+ remarkPlugins={[remarkGfm]}
546
+ components={{
547
+ p: ({ children }) => <p className="mb-3 last:mb-0 leading-relaxed">{children}</p>,
548
+ strong: ({ children }) => <strong className="font-semibold text-foreground">{children}</strong>,
549
+ ul: ({ children }) => <ul className="list-disc list-inside mb-3 space-y-1">{children}</ul>,
550
+ ol: ({ children }) => <ol className="list-decimal list-inside mb-3 space-y-1">{children}</ol>,
551
+ li: ({ children }) => <li className="leading-relaxed">{children}</li>,
552
+ a: ({ href, children }) => (
553
+ <a
554
+ href={href}
555
+ className="text-primary hover:underline underline-offset-2"
556
+ target="_blank"
557
+ rel="noopener noreferrer"
558
+ >
559
+ {children}
560
+ </a>
561
+ ),
562
+ h1: ({ children }) => <h1 className="text-lg font-bold mb-2 mt-4 first:mt-0">{children}</h1>,
563
+ h2: ({ children }) => <h2 className="text-base font-bold mb-2 mt-3 first:mt-0">{children}</h2>,
564
+ h3: ({ children }) => <h3 className="text-sm font-semibold mb-2 mt-2 first:mt-0">{children}</h3>,
565
+ code: ({ children }) => (
566
+ <code className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono">{children}</code>
567
+ ),
568
+ pre: ({ children }) => (
569
+ <pre className="bg-muted p-3 rounded-lg overflow-x-auto mb-3 text-xs">{children}</pre>
570
+ ),
571
+ }}
572
+ >
573
+ {m.content}
574
+ </ReactMarkdown>
575
+ </div>
576
+ ) : (
577
+ <div className="wrap-break-word">{m.content}</div>
578
+ )}
579
+ </div>
580
+ <span className="text-[10px] text-muted-foreground px-1">
581
+ {m.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
582
+ </span>
583
+ </div>
584
+ </div>
585
+ ))}
586
+ {isTyping && (
587
+ <div className="flex gap-3 animate-in fade-in">
588
+ <Avatar className="h-8 w-8 border bg-primary/10">
589
+ <AvatarFallback className="text-primary">
590
+ <Scale className="h-4 w-4" />
591
+ </AvatarFallback>
592
+ </Avatar>
593
+ <div className="bg-muted/80 border rounded-2xl rounded-tl-none px-4 py-3 flex gap-1">
594
+ <span
595
+ className="h-1.5 w-1.5 rounded-full bg-primary/40 animate-bounce"
596
+ style={{ animationDelay: "0ms" }}
597
+ />
598
+ <span
599
+ className="h-1.5 w-1.5 rounded-full bg-primary/40 animate-bounce"
600
+ style={{ animationDelay: "150ms" }}
601
+ />
602
+ <span
603
+ className="h-1.5 w-1.5 rounded-full bg-primary/40 animate-bounce"
604
+ style={{ animationDelay: "300ms" }}
605
+ />
606
+ </div>
607
+ </div>
608
+ )}
609
+ </div>
610
+ </ScrollArea>
611
+ </CardContent>
612
+
613
+ <CardFooter className="p-4 border-t bg-muted/20 backdrop-blur-md">
614
+ <div className="w-full space-y-4 max-w-4xl mx-auto">
615
+ {messages.length < 3 && (
616
+ <div className="flex flex-wrap gap-2">
617
+ {suggestedQuestions.map((q) => (
618
+ <Badge
619
+ key={q}
620
+ variant="outline"
621
+ className="cursor-pointer hover:bg-primary/10 hover:border-primary/30 transition-colors px-3 py-1 bg-background"
622
+ onClick={() => handleSend(q)}
623
+ >
624
+ {q}
625
+ </Badge>
626
+ ))}
627
+ </div>
628
+ )}
629
+ <div className="flex gap-2 relative">
630
+ <Input
631
+ placeholder="Type your legal question here..."
632
+ value={input}
633
+ onChange={(e) => setInput(e.target.value)}
634
+ onKeyDown={(e) => e.key === "Enter" && handleSend(input)}
635
+ className="pr-12 h-12 bg-background border-primary/20 focus-visible:ring-primary shadow-inner"
636
+ />
637
+ <Button
638
+ size="icon"
639
+ onClick={() => handleSend(input)}
640
+ className="absolute right-1 top-1 h-10 w-10 bg-primary hover:bg-primary/90 transition-transform active:scale-95"
641
+ >
642
+ <Send className="h-4 w-4" />
643
+ </Button>
644
+ </div>
645
+ <p className="text-[10px] text-center text-muted-foreground">
646
+ AI responses are for information only and not professional legal advice.
647
+ </p>
648
+ </div>
649
+ </CardFooter>
650
+ </Card>
651
+ </div>
652
+
653
+ {/* Letter Generation Confirmation Card */}
654
+ {showLetterDialog && pendingAction && (
655
+ <Card className="fixed bottom-6 right-6 z-50 shadow-2xl border-primary/20 w-[90vw] sm:w-96 animate-in slide-in-from-bottom-4 fade-in duration-300">
656
+ <CardHeader className="pb-3">
657
+ <CardTitle className="text-base">Generate Letter</CardTitle>
658
+ <p className="text-sm text-muted-foreground mt-1">
659
+ Would you like me to help you draft a {pendingAction.letter_type}?
660
+ </p>
661
+ </CardHeader>
662
+ <CardFooter className="gap-2 pt-3">
663
+ <Button variant="outline" onClick={cancelLetterGeneration} className="flex-1">
664
+ No, thanks
665
+ </Button>
666
+ <Button onClick={confirmLetterGeneration} className="flex-1">
667
+ Yes, generate it
668
+ </Button>
669
+ </CardFooter>
670
+ </Card>
671
+ )}
672
+ </div>
673
+ )
674
+ }
Frontend/components/chatbot/letter-generator.tsx ADDED
@@ -0,0 +1,739 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useRef, useEffect } from "react"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Input } from "@/components/ui/input"
6
+ import { ScrollArea } from "@/components/ui/scroll-area"
7
+ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
8
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
9
+ import { Badge } from "@/components/ui/badge"
10
+ import { Send, FileText, User, Download, Loader2, CheckCircle2, Printer } from "lucide-react"
11
+ import { cn } from "@/lib/utils"
12
+ import ReactMarkdown from "react-markdown"
13
+ import remarkGfm from "remark-gfm"
14
+ import html2canvas from "html2canvas"
15
+ import { addDocumentToCache } from "@/lib/document-cache"
16
+
17
+ type MessageRole = "user" | "assistant" | "system"
18
+
19
+ interface Message {
20
+ id: string
21
+ role: MessageRole
22
+ content: string
23
+ timestamp: Date
24
+ }
25
+
26
+ type ConversationState = "initial" | "template-search" | "collecting-placeholders" | "generating" | "completed"
27
+
28
+ interface PlaceholderData {
29
+ [key: string]: string
30
+ }
31
+
32
+ export function LetterGenerator() {
33
+ const [messages, setMessages] = useState<Message[]>([
34
+ {
35
+ id: "1",
36
+ role: "assistant",
37
+ content: "Namaste! I am your Letter Generation Assistant. What kind of letter would you like to generate? (e.g., citizenship application, leave application, complaint letter, etc.)",
38
+ timestamp: new Date(),
39
+ },
40
+ ])
41
+ const [input, setInput] = useState("")
42
+ const [isTyping, setIsTyping] = useState(false)
43
+ const [conversationState, setConversationState] = useState<ConversationState>("initial")
44
+ const [templateName, setTemplateName] = useState<string | null>(null)
45
+ const [placeholders, setPlaceholders] = useState<string[]>([])
46
+ const [placeholderData, setPlaceholderData] = useState<PlaceholderData>({})
47
+ const [currentPlaceholderIndex, setCurrentPlaceholderIndex] = useState(0)
48
+ const [generatedLetter, setGeneratedLetter] = useState<string | null>(null)
49
+ const [generatedLetterImage, setGeneratedLetterImage] = useState<string | null>(null)
50
+ const [isGeneratingImage, setIsGeneratingImage] = useState(false)
51
+ const scrollAreaRef = useRef<HTMLDivElement>(null)
52
+ const letterTemplateRef = useRef<HTMLDivElement>(null)
53
+
54
+ useEffect(() => {
55
+ const timer = setTimeout(() => {
56
+ if (scrollAreaRef.current) {
57
+ const scrollContainer = scrollAreaRef.current.querySelector("[data-radix-scroll-area-viewport]")
58
+ if (scrollContainer) {
59
+ scrollContainer.scrollTo({
60
+ top: scrollContainer.scrollHeight,
61
+ behavior: "smooth",
62
+ })
63
+ }
64
+ }
65
+ }, 100)
66
+ return () => clearTimeout(timer)
67
+ }, [messages, isTyping])
68
+
69
+ // Auto-fill prompt from chatbot redirect
70
+ useEffect(() => {
71
+ const prompt = localStorage.getItem("letter_generation_prompt")
72
+ const letterType = localStorage.getItem("letter_type")
73
+
74
+ if (prompt) {
75
+ // Clear the stored data
76
+ localStorage.removeItem("letter_generation_prompt")
77
+ localStorage.removeItem("letter_type")
78
+
79
+ // Auto-send the prompt
80
+ setInput(prompt)
81
+ setTimeout(() => {
82
+ handleSend(prompt)
83
+ }, 500)
84
+ }
85
+ }, [])
86
+
87
+ const addMessage = (role: MessageRole, content: string) => {
88
+ const newMessage: Message = {
89
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // Unique ID
90
+ role,
91
+ content,
92
+ timestamp: new Date(),
93
+ }
94
+ setMessages((prev) => [...prev, newMessage])
95
+ }
96
+
97
+ const handleSend = async (content: string) => {
98
+ if (!content.trim()) return
99
+
100
+ addMessage("user", content)
101
+ setInput("")
102
+ setIsTyping(true)
103
+
104
+ try {
105
+ if (conversationState === "initial" || conversationState === "template-search") {
106
+ // Step 1: Search for template
107
+ await searchTemplate(content)
108
+ } else if (conversationState === "collecting-placeholders") {
109
+ // Step 2: Collect placeholder values
110
+ await collectPlaceholder(content)
111
+ }
112
+ } catch (error) {
113
+ console.error("[Letter Generation Error]:", error)
114
+ addMessage("assistant", "I encountered an error. Please try again or rephrase your request.")
115
+ } finally {
116
+ setIsTyping(false)
117
+ }
118
+ }
119
+
120
+ const searchTemplate = async (query: string) => {
121
+ try {
122
+ const token = localStorage.getItem("access_token")
123
+ const headers: Record<string, string> = { "Content-Type": "application/json" }
124
+ if (token) {
125
+ headers["Authorization"] = `Bearer ${token}`
126
+ }
127
+
128
+ const response = await fetch("/api/letter-generation", {
129
+ method: "POST",
130
+ headers,
131
+ body: JSON.stringify({
132
+ action: "search-template",
133
+ data: { query },
134
+ }),
135
+ })
136
+
137
+ const data = await response.json()
138
+
139
+ if (data.success && data.templateName) {
140
+ setTemplateName(data.templateName)
141
+ addMessage(
142
+ "assistant",
143
+ `I found a template: **${data.templateName}**. Let me get the required information for this letter.`
144
+ )
145
+
146
+ // Get template details
147
+ await getTemplateDetails(data.templateName)
148
+ } else {
149
+ addMessage(
150
+ "assistant",
151
+ "I couldn't find a suitable template for your request. Could you please provide more details about the type of letter you need?"
152
+ )
153
+ setConversationState("template-search")
154
+ }
155
+ } catch (error) {
156
+ throw error
157
+ }
158
+ }
159
+
160
+ const getTemplateDetails = async (templateName: string) => {
161
+ try {
162
+ const token = localStorage.getItem("access_token")
163
+ const headers: Record<string, string> = { "Content-Type": "application/json" }
164
+ if (token) {
165
+ headers["Authorization"] = `Bearer ${token}`
166
+ }
167
+
168
+ const response = await fetch("/api/letter-generation", {
169
+ method: "POST",
170
+ headers,
171
+ body: JSON.stringify({
172
+ action: "get-template-details",
173
+ data: { templateName },
174
+ }),
175
+ })
176
+
177
+ const data = await response.json()
178
+
179
+ if (data.success && data.placeholders) {
180
+ setPlaceholders(data.placeholders)
181
+ setCurrentPlaceholderIndex(0)
182
+ setConversationState("collecting-placeholders")
183
+
184
+ if (data.placeholders.length > 0) {
185
+ const firstPlaceholder = data.placeholders[0]
186
+ addMessage(
187
+ "assistant",
188
+ `Great! I need some information from you. Please provide the **${formatPlaceholder(firstPlaceholder)}**:`
189
+ )
190
+ } else {
191
+ // No placeholders needed, generate directly
192
+ await fillTemplate(templateName, {})
193
+ }
194
+ } else {
195
+ addMessage("assistant", "I encountered an issue retrieving the template details. Please try again.")
196
+ }
197
+ } catch (error) {
198
+ throw error
199
+ }
200
+ }
201
+
202
+ const collectPlaceholder = async (value: string) => {
203
+ const currentPlaceholder = placeholders[currentPlaceholderIndex]
204
+ const newPlaceholderData = {
205
+ ...placeholderData,
206
+ [currentPlaceholder]: value,
207
+ }
208
+ setPlaceholderData(newPlaceholderData)
209
+
210
+ const nextIndex = currentPlaceholderIndex + 1
211
+
212
+ if (nextIndex < placeholders.length) {
213
+ // Ask for next placeholder
214
+ setCurrentPlaceholderIndex(nextIndex)
215
+ const nextPlaceholder = placeholders[nextIndex]
216
+ addMessage("assistant", `Thank you! Now, please provide the **${formatPlaceholder(nextPlaceholder)}**:`)
217
+ } else {
218
+ // All placeholders collected, generate letter
219
+ setConversationState("generating")
220
+ addMessage("assistant", "Perfect! I have all the information I need. Generating your letter...")
221
+ await fillTemplate(templateName!, newPlaceholderData)
222
+ }
223
+ }
224
+
225
+ const fillTemplate = async (templateName: string, placeholders: PlaceholderData) => {
226
+ try {
227
+ const token = localStorage.getItem("access_token")
228
+ const headers: Record<string, string> = { "Content-Type": "application/json" }
229
+ if (token) {
230
+ headers["Authorization"] = `Bearer ${token}`
231
+ }
232
+
233
+ const response = await fetch("/api/letter-generation", {
234
+ method: "POST",
235
+ headers,
236
+ body: JSON.stringify({
237
+ action: "fill-template",
238
+ data: { templateName, placeholders },
239
+ }),
240
+ })
241
+
242
+ const data = await response.json()
243
+
244
+ if (data.success && data.letter) {
245
+ setGeneratedLetter(data.letter)
246
+ setConversationState("completed")
247
+ addMessage(
248
+ "assistant",
249
+ `✅ Your letter has been generated successfully! You can download it using the button below.\n\n**Preview:**\n\n${data.letter}`
250
+ )
251
+
252
+ // Cache the generated letter
253
+ addDocumentToCache({
254
+ filename: `${templateName || "letter"}_${Date.now()}.txt`,
255
+ type: "letter-generation",
256
+ result: {
257
+ success: true,
258
+ },
259
+ })
260
+ } else {
261
+ addMessage("assistant", "Failed to generate the letter. Please try again.")
262
+ setConversationState("initial")
263
+ }
264
+ } catch (error) {
265
+ throw error
266
+ }
267
+ }
268
+
269
+ const formatPlaceholder = (placeholder: string): string => {
270
+ return placeholder
271
+ .replace(/{|}/g, "")
272
+ .replace(/_/g, " ")
273
+ .replace(/\b\w/g, (char) => char.toUpperCase())
274
+ }
275
+
276
+ const generateLetterImage = async () => {
277
+ if (!letterTemplateRef.current) {
278
+ console.log("Letter template ref not available")
279
+ return
280
+ }
281
+
282
+ setIsGeneratingImage(true)
283
+ try {
284
+ console.log("Starting image generation...")
285
+
286
+ // Create an iframe to completely isolate rendering context
287
+ const iframe = document.createElement('iframe')
288
+ iframe.style.cssText = 'position: fixed; left: -10000px; top: 0; width: 794px; height: 1123px; border: none;'
289
+ document.body.appendChild(iframe)
290
+
291
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document
292
+ if (!iframeDoc) throw new Error('Unable to access iframe document')
293
+
294
+ // Write clean HTML with only our content
295
+ iframeDoc.open()
296
+ iframeDoc.write(`
297
+ <!DOCTYPE html>
298
+ <html>
299
+ <head>
300
+ <meta charset="UTF-8">
301
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@400;600;700&display=swap" rel="stylesheet">
302
+ <style>
303
+ * { margin: 10; padding: 10; box-sizing: border-box; }
304
+ body { background: white; margin: 0; padding: 0; }
305
+ </style>
306
+ </head>
307
+ <body>${letterTemplateRef.current.innerHTML}</body>
308
+ </html>
309
+ `)
310
+ iframeDoc.close()
311
+
312
+ // Wait for fonts to load
313
+ await new Promise(resolve => setTimeout(resolve, 500))
314
+
315
+ const canvas = await html2canvas(iframeDoc.body, {
316
+ scale: 2,
317
+ useCORS: true,
318
+ allowTaint: true,
319
+ backgroundColor: '#ffffff',
320
+ logging: false,
321
+ width: 794,
322
+ height: 1123,
323
+ })
324
+
325
+ // Clean up
326
+ document.body.removeChild(iframe)
327
+
328
+ console.log("Canvas created, converting to data URL...")
329
+ const dataUrl = canvas.toDataURL("image/png", 1.0)
330
+ console.log("Image generated successfully")
331
+ setGeneratedLetterImage(dataUrl)
332
+ setIsGeneratingImage(false)
333
+ } catch (error) {
334
+ console.error("Image generation failed:", error)
335
+ setIsGeneratingImage(false)
336
+ addMessage("assistant", "Failed to generate letter image. You can still download the text version.")
337
+ }
338
+ }
339
+
340
+ useEffect(() => {
341
+ if (generatedLetter && conversationState === "completed") {
342
+ // Generate image after a short delay to ensure DOM is rendered
343
+ setTimeout(() => {
344
+ generateLetterImage()
345
+ }, 500)
346
+ }
347
+ }, [generatedLetter, conversationState])
348
+
349
+ const downloadLetterImage = () => {
350
+ if (!generatedLetterImage) {
351
+ console.log("No image available to download")
352
+ return
353
+ }
354
+
355
+ try {
356
+ console.log("Starting download...")
357
+ const link = document.createElement("a")
358
+ link.href = generatedLetterImage
359
+ link.download = `${templateName || "letter"}_${Date.now()}.png`
360
+ document.body.appendChild(link)
361
+ link.click()
362
+ document.body.removeChild(link)
363
+ console.log("Download initiated successfully")
364
+ } catch (error) {
365
+ console.error("Download failed:", error)
366
+ addMessage("assistant", "Download failed. Please try the Print option or download as text.")
367
+ }
368
+ }
369
+
370
+ const downloadLetterText = () => {
371
+ if (!generatedLetter) return
372
+
373
+ const blob = new Blob([generatedLetter], { type: "text/plain;charset=utf-8" })
374
+ const url = URL.createObjectURL(blob)
375
+ const a = document.createElement("a")
376
+ a.href = url
377
+ a.download = `${templateName || "letter"}_${Date.now()}.txt`
378
+ a.click()
379
+ URL.revokeObjectURL(url)
380
+ }
381
+
382
+ const printLetter = () => {
383
+ if (!generatedLetterImage) return
384
+
385
+ const printWindow = window.open("", "", "width=800,height=600")
386
+ if (!printWindow) return
387
+
388
+ printWindow.document.write(`
389
+ <html>
390
+ <head>
391
+ <title>Print Letter</title>
392
+ <style>
393
+ @page { size: A4; margin: 0; }
394
+ body { margin: 10; padding: 10; }
395
+ img { width: 80%; height: auto; display: block; }
396
+ </style>
397
+ </head>
398
+ <body>
399
+ <img src="${generatedLetterImage}" onload="window.print(); window.close();" />
400
+ </body>
401
+ </html>
402
+ `)
403
+ printWindow.document.close()
404
+ }
405
+
406
+ const startNew = () => {
407
+ setConversationState("initial")
408
+ setTemplateName(null)
409
+ setPlaceholders([])
410
+ setPlaceholderData({})
411
+ setCurrentPlaceholderIndex(0)
412
+ setGeneratedLetter(null)
413
+ setGeneratedLetterImage(null)
414
+ setMessages([
415
+ {
416
+ id: Date.now().toString(),
417
+ role: "assistant",
418
+ content:
419
+ "Let's generate another letter! What kind of letter would you like to create?",
420
+ timestamp: new Date(),
421
+ },
422
+ ])
423
+ }
424
+
425
+ return (
426
+ <div className="w-full max-w-[1600px] mx-auto">
427
+ <div className="flex h-[calc(100vh-10rem)] gap-4">
428
+ {/* Main Chat Interface */}
429
+ <Card className="flex-1 flex flex-col shadow-lg border-primary/10 overflow-hidden bg-card/50 backdrop-blur-sm">
430
+ <CardHeader className="border-b py-4 flex flex-row items-center justify-between">
431
+ <div className="flex items-center gap-3">
432
+ <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
433
+ <FileText className="h-5 w-5" />
434
+ </div>
435
+ <div>
436
+ <CardTitle className="text-lg">Letter Generation Assistant</CardTitle>
437
+ <div className="flex items-center gap-2">
438
+ <span className="h-2 w-2 rounded-full bg-success animate-pulse" />
439
+ <span className="text-xs text-muted-foreground">AI Powered & Ready</span>
440
+ </div>
441
+ </div>
442
+ </div>
443
+ {conversationState === "completed" && (
444
+ <div className="flex gap-2">
445
+ <Button
446
+ variant="default"
447
+ size="sm"
448
+ onClick={downloadLetterImage}
449
+ disabled={!generatedLetterImage || isGeneratingImage}
450
+ >
451
+ <Download className="h-4 w-4 mr-2" />
452
+ {isGeneratingImage ? "Generating..." : "Download Image"}
453
+ </Button>
454
+ <Button
455
+ variant="outline"
456
+ size="sm"
457
+ onClick={printLetter}
458
+ disabled={!generatedLetterImage || isGeneratingImage}
459
+ >
460
+ <Printer className="h-4 w-4 mr-2" />
461
+ Print
462
+ </Button>
463
+ <Button variant="outline" size="sm" onClick={downloadLetterText}>
464
+ <FileText className="h-4 w-4 mr-2" />
465
+ Download Text
466
+ </Button>
467
+ <Button variant="secondary" size="sm" onClick={startNew}>
468
+ <FileText className="h-4 w-4 mr-2" />
469
+ New Letter
470
+ </Button>
471
+ </div>
472
+ )}
473
+ </CardHeader>
474
+
475
+ <CardContent className="flex-1 p-0 overflow-hidden">
476
+ <ScrollArea ref={scrollAreaRef} className="h-full">
477
+ <div className="space-y-6 max-w-4xl mx-auto p-4 md:p-6">
478
+ {messages.map((m) => (
479
+ <div
480
+ key={m.id}
481
+ className={cn(
482
+ "flex gap-3 animate-in fade-in slide-in-from-bottom-2",
483
+ m.role === "user" ? "flex-row-reverse" : "flex-row"
484
+ )}
485
+ >
486
+ <Avatar className={cn("h-8 w-8 border", m.role === "assistant" ? "bg-primary/10" : "bg-muted")}>
487
+ <AvatarFallback className={m.role === "assistant" ? "text-primary" : "text-muted-foreground"}>
488
+ {m.role === "assistant" ? <FileText className="h-4 w-4" /> : <User className="h-4 w-4" />}
489
+ </AvatarFallback>
490
+ </Avatar>
491
+ <div
492
+ className={cn("flex flex-col gap-1 max-w-[80%]", m.role === "user" ? "items-end" : "items-start")}
493
+ >
494
+ <div
495
+ className={cn(
496
+ "rounded-2xl px-4 py-3 text-sm shadow-sm",
497
+ m.role === "user"
498
+ ? "bg-primary text-primary-foreground rounded-tr-none"
499
+ : "bg-muted/80 backdrop-blur-sm rounded-tl-none border"
500
+ )}
501
+ >
502
+ {m.role === "assistant" ? (
503
+ <div className="prose prose-sm dark:prose-invert max-w-none overflow-wrap-anywhere wrap-break-word">
504
+ <ReactMarkdown
505
+ remarkPlugins={[remarkGfm]}
506
+ components={{
507
+ p: ({ children }) => <p className="mb-3 last:mb-0 leading-relaxed">{children}</p>,
508
+ strong: ({ children }) => <strong className="font-semibold text-foreground">{children}</strong>,
509
+ ul: ({ children }) => <ul className="list-disc list-inside mb-3 space-y-1">{children}</ul>,
510
+ ol: ({ children }) => <ol className="list-decimal list-inside mb-3 space-y-1">{children}</ol>,
511
+ li: ({ children }) => <li className="leading-relaxed">{children}</li>,
512
+ code: ({ children }) => (
513
+ <code className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono">{children}</code>
514
+ ),
515
+ pre: ({ children }) => (
516
+ <pre className="bg-muted p-3 rounded-lg overflow-x-auto mb-3 text-xs whitespace-pre-wrap">
517
+ {children}
518
+ </pre>
519
+ ),
520
+ }}
521
+ >
522
+ {m.content}
523
+ </ReactMarkdown>
524
+ </div>
525
+ ) : (
526
+ <div className="wrap-break-word">{m.content}</div>
527
+ )}
528
+ </div>
529
+ <span className="text-[10px] text-muted-foreground px-1">
530
+ {m.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
531
+ </span>
532
+ </div>
533
+ </div>
534
+ ))}
535
+ {isTyping && (
536
+ <div className="flex gap-3 animate-in fade-in">
537
+ <Avatar className="h-8 w-8 border bg-primary/10">
538
+ <AvatarFallback className="text-primary">
539
+ <FileText className="h-4 w-4" />
540
+ </AvatarFallback>
541
+ </Avatar>
542
+ <div className="bg-muted/80 border rounded-2xl rounded-tl-none px-4 py-3 flex gap-1">
543
+ <span
544
+ className="h-1.5 w-1.5 rounded-full bg-primary/40 animate-bounce"
545
+ style={{ animationDelay: "0ms" }}
546
+ />
547
+ <span
548
+ className="h-1.5 w-1.5 rounded-full bg-primary/40 animate-bounce"
549
+ style={{ animationDelay: "150ms" }}
550
+ />
551
+ <span
552
+ className="h-1.5 w-1.5 rounded-full bg-primary/40 animate-bounce"
553
+ style={{ animationDelay: "300ms" }}
554
+ />
555
+ </div>
556
+ </div>
557
+ )}
558
+ </div>
559
+ </ScrollArea>
560
+
561
+ {/* Preview of generated letter image */}
562
+ {conversationState === "completed" && (
563
+ <div className="p-4 border-t bg-muted/10">
564
+ <div className="max-w-4xl mx-auto">
565
+ <div className="flex justify-between items-center mb-3">
566
+ <h3 className="text-sm font-semibold">Generated Letter Preview (Nepali Format):</h3>
567
+ {!generatedLetterImage && !isGeneratingImage && (
568
+ <Button size="sm" variant="outline" onClick={generateLetterImage}>
569
+ Generate Image
570
+ </Button>
571
+ )}
572
+ {isGeneratingImage && (
573
+ <span className="text-xs text-muted-foreground flex items-center gap-2">
574
+ <Loader2 className="h-3 w-3 animate-spin" />
575
+ Generating image...
576
+ </span>
577
+ )}
578
+ </div>
579
+ {generatedLetterImage ? (
580
+ <div className="border rounded-lg overflow-hidden shadow-lg bg-white">
581
+ <img src={generatedLetterImage} alt="Nepali Letter" className="w-full" />
582
+ </div>
583
+ ) : (
584
+ <div className="border rounded-lg p-8 text-center bg-muted/50">
585
+ <p className="text-sm text-muted-foreground">
586
+ Click "Generate Image" to create the Nepali format version
587
+ </p>
588
+ </div>
589
+ )}
590
+ </div>
591
+ </div>
592
+ )}
593
+ </CardContent>
594
+
595
+ <CardFooter className="p-4 border-t bg-muted/20 backdrop-blur-md">
596
+ <div className="w-full space-y-4 max-w-4xl mx-auto">
597
+ {conversationState !== "completed" && (
598
+ <div className="flex gap-2 relative">
599
+ <Input
600
+ placeholder={
601
+ conversationState === "collecting-placeholders"
602
+ ? `Enter ${formatPlaceholder(placeholders[currentPlaceholderIndex] || "")}...`
603
+ : "Describe the letter you need..."
604
+ }
605
+ value={input}
606
+ onChange={(e) => setInput(e.target.value)}
607
+ onKeyDown={(e) => e.key === "Enter" && handleSend(input)}
608
+ className="pr-12 h-12 bg-background border-primary/20 focus-visible:ring-primary shadow-inner"
609
+ disabled={isTyping}
610
+ />
611
+ <Button
612
+ size="icon"
613
+ onClick={() => handleSend(input)}
614
+ className="absolute right-1 top-1 h-10 w-10 bg-primary hover:bg-primary/90 transition-transform active:scale-95"
615
+ disabled={isTyping || !input.trim()}
616
+ >
617
+ {isTyping ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
618
+ </Button>
619
+ </div>
620
+ )}
621
+ <p className="text-[10px] text-center text-muted-foreground">
622
+ AI-generated letters should be reviewed before official use.
623
+ </p>
624
+ </div>
625
+ </CardFooter>
626
+ </Card>
627
+ </div>
628
+
629
+ {/* Hidden Nepali Paper Template for Image Generation */}
630
+ {generatedLetter && (
631
+ <div
632
+ ref={letterTemplateRef}
633
+ style={{
634
+ position: "fixed",
635
+ left: "-10000px",
636
+ top: "90",
637
+ width: "794px", // A4 width at 96 DPI
638
+ minHeight: "1123px", // A4 height at 96 DPI
639
+ backgroundColor: "rgb(255, 255, 255)",
640
+ fontFamily: '"Noto Sans Devanagari", "Kalimati", Arial, sans-serif',
641
+ fontSize: "14pt",
642
+ lineHeight: "1.8",
643
+ color: "rgb(0, 0, 0)",
644
+ padding: "80px 80px",
645
+ boxSizing: "border-box",
646
+ visibility: "hidden",
647
+ pointerEvents: "none",
648
+ border: "none",
649
+ margin: "10",
650
+ isolation: "isolate", // Isolate from parent styles
651
+ }}
652
+ >
653
+ {/* Reference Number (Date removed) */}
654
+ <div
655
+ style={{
656
+ marginBottom: "24px",
657
+ fontSize: "11pt",
658
+ fontWeight: "600",
659
+ color: "rgb(0, 0, 0)",
660
+ }}
661
+ >
662
+ <div style={{marginLeft:"40px",marginTop:"50px"}}>चि. नं.: {placeholderData.reference_no || placeholderData.ref_no || "............."}</div>
663
+ </div>
664
+
665
+ {/* Recipient Address */}
666
+ {(placeholderData.recipient_name || placeholderData.receiver_name) && (
667
+ <div style={{ marginBottom: "24px", fontSize: "12pt", color: "rgb(0, 0, 0)",marginRight:"30px", }}>
668
+ <div style={{ fontWeight: "600" }}>श्रीमान्,</div>
669
+ <div>{placeholderData.recipient_name || placeholderData.receiver_name}</div>
670
+ {placeholderData.recipient_designation && <div>{placeholderData.recipient_designation}</div>}
671
+ {placeholderData.recipient_address && <div>{placeholderData.recipient_address}</div>}
672
+ </div>
673
+ )}
674
+
675
+ {/* Subject */}
676
+ {placeholderData.subject && (
677
+ <div style={{ marginBottom: "20px", fontSize: "12pt", fontWeight: "600", color: "rgb(0, 0, 0)",marginLeft:"50px",marginRight:"30px", }}>
678
+ <strong>विषय:</strong> {placeholderData.subject}
679
+ </div>
680
+ )}
681
+
682
+ {/* Salutation */}
683
+ <div style={{ marginBottom: "16px", fontSize: "12pt", color: "rgb(0, 0, 0)",marginLeft:"40px",marginRight:"30px", }}>महोदय,</div>
684
+
685
+ {/* Main Letter Content */}
686
+ <div
687
+ style={{
688
+ marginBottom: "40px",
689
+ marginLeft: "40px",
690
+ marginRight:"30px",
691
+ textAlign: "justify",
692
+ whiteSpace: "pre-wrap",
693
+ fontSize: "12pt",
694
+ lineHeight: "2",
695
+ color: "rgb(0, 0, 0)",
696
+ }}
697
+ >
698
+ {generatedLetter}
699
+ </div>
700
+
701
+ {/* Signature Section */}
702
+ {/* <div style={{ marginTop: "60px", float: "right", textAlign: "center", fontSize: "12pt", color: "rgb(0, 0, 0)" }}>
703
+ <div
704
+ style={{
705
+ borderTop: "2px solid rgb(0, 0, 0)",
706
+ paddingTop: "8px",
707
+ minWidth: "200px",
708
+ marginTop: "60px",
709
+ }}
710
+ >
711
+ <div style={{ fontWeight: "700", marginBottom: "4px", color: "rgb(0, 0, 0)" }}>
712
+ {placeholderData.sender_name || placeholderData.name || "........................."}
713
+ </div>
714
+ <div style={{ fontSize: "11pt", color: "rgb(55, 65, 81)" }}>
715
+ {placeholderData.sender_designation ||
716
+ placeholderData.designation ||
717
+ placeholderData.position ||
718
+ "........................."}
719
+ </div>
720
+ {placeholderData.sender_office && (
721
+ <div style={{ fontSize: "10pt", color: "rgb(107, 114, 128)", marginTop: "2px" }}>
722
+ {placeholderData.sender_office}
723
+ </div>
724
+ )}
725
+ </div>
726
+ </div> */}
727
+
728
+ {/* Footer with contact info (if available) */}
729
+ <div style={{ clear: "both", marginTop: "100px", paddingTop: "20px", borderTop: "1px solid rgb(229, 231, 235)" }}>
730
+ <div style={{ fontSize: "9pt", color: "rgb(107, 114, 128)", textAlign: "center" }}>
731
+ {placeholderData.contact_email && <span>Email: {placeholderData.contact_email} | </span>}
732
+ {placeholderData.contact_phone && <span>Phone: {placeholderData.contact_phone}</span>}
733
+ </div>
734
+ </div>
735
+ </div>
736
+ )}
737
+ </div>
738
+ )
739
+ }
Frontend/components/common/file-upload.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import type React from "react"
4
+
5
+ import { useState, useRef } from "react"
6
+ import { Button } from "@/components/ui/button"
7
+ import { Upload, X, CheckCircle2 } from "lucide-react"
8
+ import { cn } from "@/lib/utils"
9
+
10
+ interface FileUploadProps {
11
+ onFileSelect: (text: string) => void
12
+ accept?: string
13
+ className?: string
14
+ }
15
+
16
+ export function FileUpload({ onFileSelect, accept = ".txt,.pdf", className }: FileUploadProps) {
17
+ const [dragActive, setDragActive] = useState(false)
18
+ const [fileName, setFileName] = useState<string | null>(null)
19
+ const fileInputRef = useRef<HTMLInputElement>(null)
20
+
21
+ const handleDrag = (e: React.DragEvent) => {
22
+ e.preventDefault()
23
+ e.stopPropagation()
24
+ if (e.type === "dragenter" || e.type === "dragover") {
25
+ setDragActive(true)
26
+ } else if (e.type === "dragleave") {
27
+ setDragActive(false)
28
+ }
29
+ }
30
+
31
+ const processFile = async (file: File) => {
32
+ setFileName(file.name)
33
+ if (file.type === "text/plain") {
34
+ const text = await file.text()
35
+ onFileSelect(text)
36
+ } else {
37
+ // PDF processing would normally happen here via a library
38
+ // For this demo, we'll simulate it
39
+ onFileSelect(`Simulated text content from: ${file.name}\n\nThis legal notice contains various clauses...`)
40
+ }
41
+ }
42
+
43
+ const handleDrop = (e: React.DragEvent) => {
44
+ e.preventDefault()
45
+ setDragActive(false)
46
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
47
+ processFile(e.dataTransfer.files[0])
48
+ }
49
+ }
50
+
51
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
52
+ if (e.target.files && e.target.files[0]) {
53
+ processFile(e.target.files[0])
54
+ }
55
+ }
56
+
57
+ return (
58
+ <div className={cn("relative", className)}>
59
+ <input
60
+ ref={fileInputRef}
61
+ type="file"
62
+ accept={accept}
63
+ onChange={handleChange}
64
+ className="hidden"
65
+ id="file-upload"
66
+ />
67
+ <label
68
+ htmlFor="file-upload"
69
+ onDragEnter={handleDrag}
70
+ onDragLeave={handleDrag}
71
+ onDragOver={handleDrag}
72
+ onDrop={handleDrop}
73
+ className={cn(
74
+ "flex flex-col items-center justify-center w-full h-40 border-2 border-dashed rounded-xl cursor-pointer transition-all duration-200",
75
+ dragActive
76
+ ? "border-primary bg-primary/5 scale-[1.01]"
77
+ : "border-muted hover:border-primary/50 hover:bg-muted/30",
78
+ fileName ? "border-success/50 bg-success/5" : "",
79
+ )}
80
+ >
81
+ {fileName ? (
82
+ <div className="flex flex-col items-center gap-2">
83
+ <CheckCircle2 className="h-10 w-10 text-success" />
84
+ <p className="text-sm font-medium">{fileName}</p>
85
+ <Button
86
+ variant="ghost"
87
+ size="sm"
88
+ className="h-7 text-xs"
89
+ onClick={(e) => {
90
+ e.preventDefault()
91
+ setFileName(null)
92
+ if (fileInputRef.current) fileInputRef.current.value = ""
93
+ }}
94
+ >
95
+ <X className="h-3 w-3 mr-1" /> Remove
96
+ </Button>
97
+ </div>
98
+ ) : (
99
+ <div className="flex flex-col items-center gap-2 p-6 text-center">
100
+ <Upload className="h-10 w-10 text-muted-foreground mb-2" />
101
+ <p className="text-sm font-semibold">Click or drag to upload document</p>
102
+ <p className="text-xs text-muted-foreground">Supported formats: .txt, .pdf</p>
103
+ </div>
104
+ )}
105
+ </label>
106
+ </div>
107
+ )
108
+ }
Frontend/components/dashboard/stats-card.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent } from "@/components/ui/card"
2
+ import type { LucideIcon } from "lucide-react"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ interface StatsCardProps {
6
+ title: string
7
+ value: string | number
8
+ description: string
9
+ icon: LucideIcon
10
+ color: "primary" | "accent" | "success" | "destructive"
11
+ }
12
+
13
+ export function StatsCard({ title, value, description, icon: Icon, color }: StatsCardProps) {
14
+ const colorMap = {
15
+ primary: "bg-primary/10 text-primary border-primary/20",
16
+ accent: "bg-accent/10 text-accent border-accent/20",
17
+ success: "bg-success/10 text-success border-success/20",
18
+ destructive: "bg-destructive/10 text-destructive border-destructive/20",
19
+ }
20
+
21
+ return (
22
+ <Card className="border-primary/10 hover:shadow-md transition-shadow">
23
+ <CardContent className="p-6">
24
+ <div className="flex items-center justify-between space-y-0 pb-2">
25
+ <p className="text-sm font-medium text-muted-foreground">{title}</p>
26
+ <div className={cn("p-2 rounded-lg border", colorMap[color])}>
27
+ <Icon className="h-4 w-4" />
28
+ </div>
29
+ </div>
30
+ <div className="pt-2">
31
+ <div className="text-2xl font-bold">{value}</div>
32
+ <p className="text-xs text-muted-foreground pt-1">{description}</p>
33
+ </div>
34
+ </CardContent>
35
+ </Card>
36
+ )
37
+ }
Frontend/components/layout/footer.tsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button"
2
+ import Link from "next/link"
3
+ import { Scale, MessageSquare, ShieldCheck, BookOpen, ArrowRight } from "lucide-react"
4
+
5
+ export function Footer() {
6
+ return (
7
+ <footer className="w-full border-t bg-muted/30">
8
+ <div className="container max-w-7xl mx-auto py-12 md:py-16">
9
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
10
+ <div className="space-y-4">
11
+ <div className="flex items-center gap-2 text-primary">
12
+ <Scale className="h-6 w-6" />
13
+ <span className="font-bold text-lg">Know Your Rights</span>
14
+ </div>
15
+ <p className="text-sm text-muted-foreground">
16
+ Empowering citizens of Nepal through legal literacy and digital assistance.
17
+ </p>
18
+ {/* <div className="flex gap-4">
19
+ <Link href="#" className="text-muted-foreground hover:text-primary">
20
+ <Facebook className="h-5 w-5" />
21
+ </Link>
22
+ <Link href="#" className="text-muted-foreground hover:text-primary">
23
+ <Twitter className="h-5 w-5" />
24
+ </Link>
25
+ <Link href="#" className="text-muted-foreground hover:text-primary">
26
+ <Instagram className="h-5 w-5" />
27
+ </Link>
28
+ <Link href="#" className="text-muted-foreground hover:text-primary">
29
+ <Mail className="h-5 w-5" />
30
+ </Link>
31
+ </div> */}
32
+ </div>
33
+ <div className="md:col-span-3">
34
+ <h3 className="font-semibold mb-6 text-lg">Quick Access</h3>
35
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
36
+ <Link
37
+ href="/chatbot"
38
+ className="group relative overflow-hidden rounded-lg border bg-card p-5 hover:shadow-lg transition-all duration-300 hover:border-primary/50"
39
+ >
40
+ <div className="flex items-start gap-3">
41
+ <div className="rounded-lg bg-primary/10 p-2.5 group-hover:bg-primary/20 transition-colors">
42
+ <MessageSquare className="h-5 w-5 text-primary" />
43
+ </div>
44
+ <div className="flex-1">
45
+ <h4 className="font-semibold text-sm mb-1 group-hover:text-primary transition-colors">
46
+ Law Chatbot
47
+ </h4>
48
+ <p className="text-xs text-muted-foreground">
49
+ Get instant legal guidance
50
+ </p>
51
+ </div>
52
+ <ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
53
+ </div>
54
+ </Link>
55
+
56
+ <Link
57
+ href="/bias-checker"
58
+ className="group relative overflow-hidden rounded-lg border bg-card p-5 hover:shadow-lg transition-all duration-300 hover:border-primary/50"
59
+ >
60
+ <div className="flex items-start gap-3">
61
+ <div className="rounded-lg bg-primary/10 p-2.5 group-hover:bg-primary/20 transition-colors">
62
+ <ShieldCheck className="h-5 w-5 text-primary" />
63
+ </div>
64
+ <div className="flex-1">
65
+ <h4 className="font-semibold text-sm mb-1 group-hover:text-primary transition-colors">
66
+ Bias Checker
67
+ </h4>
68
+ <p className="text-xs text-muted-foreground">
69
+ Analyze legal documents
70
+ </p>
71
+ </div>
72
+ <ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
73
+ </div>
74
+ </Link>
75
+
76
+ <Link
77
+ href="/resources"
78
+ className="group relative overflow-hidden rounded-lg border bg-card p-5 hover:shadow-lg transition-all duration-300 hover:border-primary/50"
79
+ >
80
+ <div className="flex items-start gap-3">
81
+ <div className="rounded-lg bg-primary/10 p-2.5 group-hover:bg-primary/20 transition-colors">
82
+ <BookOpen className="h-5 w-5 text-primary" />
83
+ </div>
84
+ <div className="flex-1">
85
+ <h4 className="font-semibold text-sm mb-1 group-hover:text-primary transition-colors">
86
+ Legal Resources
87
+ </h4>
88
+ <p className="text-xs text-muted-foreground">
89
+ Browse legal information
90
+ </p>
91
+ </div>
92
+ <ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
93
+ </div>
94
+ </Link>
95
+ </div>
96
+ </div>
97
+ {/* <div>
98
+ <h3 className="font-semibold mb-4">Support</h3>
99
+ <ul className="space-y-2 text-sm">
100
+ <li>
101
+ <Link href="#" className="text-muted-foreground hover:text-primary">
102
+ Contact Us
103
+ </Link>
104
+ </li>
105
+ <li>
106
+ <Link href="#" className="text-muted-foreground hover:text-primary">
107
+ Privacy Policy
108
+ </Link>
109
+ </li>
110
+ <li>
111
+ <Link href="#" className="text-muted-foreground hover:text-primary">
112
+ Terms of Service
113
+ </Link>
114
+ </li>
115
+ </ul>
116
+ </div> */}
117
+ {/* <div className="bg-primary/5 p-6 rounded-xl border border-primary/10">
118
+ <h3 className="font-semibold mb-2">Need Urgent Help?</h3>
119
+ <p className="text-xs text-muted-foreground mb-4">
120
+ Contact our 24/7 legal helpline for immediate assistance.
121
+ </p>
122
+ <Button size="sm" className="w-full">
123
+ Call Hotline
124
+ </Button>
125
+ </div> */}
126
+ </div>
127
+ <div className="mt-12 pt-8 border-t text-center text-xs text-muted-foreground">
128
+ © {new Date().getFullYear()} Know Your Rights Nepal. All rights reserved.
129
+ </div>
130
+ </div>
131
+ </footer>
132
+ )
133
+ }
Frontend/components/layout/navbar.tsx ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { useRouter } from "next/navigation"
5
+ import { useAuth } from "@/context/auth-context"
6
+ import { Button } from "@/components/ui/button"
7
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
8
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
9
+ import { Menu, X, Scale, LogOut, LayoutDashboard, MessageSquare, Search, User } from "lucide-react"
10
+ import { useState } from "react"
11
+ import { clearAuthData } from "@/lib/auth-utils"
12
+
13
+ export function Navbar() {
14
+ const { user, logout } = useAuth()
15
+ const [isMenuOpen, setIsMenuOpen] = useState(false)
16
+ const router = useRouter()
17
+
18
+ const handleLogout = () => {
19
+ // Clear all auth data from localStorage first
20
+ clearAuthData()
21
+ // Update auth context
22
+ logout()
23
+ // Force navigation to landing page
24
+ window.location.href = "/"
25
+ }
26
+
27
+ const handleHomeClick = (e: React.MouseEvent) => {
28
+ if (user) {
29
+ e.preventDefault()
30
+ router.push("/dashboard")
31
+ }
32
+ }
33
+
34
+ // Define navigation items based on authentication status
35
+ const getNavItems = () => {
36
+ if (user) {
37
+ // Show only protected features when logged in
38
+ return [
39
+ { label: "Chatbot", href: "/chatbot", icon: MessageSquare },
40
+ { label: "Bias Checker", href: "/bias-checker", icon: Search },
41
+ { label: "Letter Generator", href: "/letter-generator" },
42
+ ]
43
+ } else {
44
+ // Show only Home when not logged in
45
+ return [
46
+ { label: "Home", href: "/" },
47
+ ]
48
+ }
49
+ }
50
+
51
+ const navItems = getNavItems()
52
+
53
+ return (
54
+ <nav className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
55
+ <div className="container flex h-16 items-center justify-between">
56
+ <div className="flex items-center gap-2">
57
+ <Link
58
+ href={user ? "/dashboard" : "/"}
59
+ onClick={handleHomeClick}
60
+ className="flex items-center gap-2 text-primary"
61
+ >
62
+ <Scale className="h-6 w-6" />
63
+ <span className="font-bold text-lg tracking-tight">Know Your Rights Nepal</span>
64
+ </Link>
65
+ </div>
66
+
67
+ {/* Desktop Navigation */}
68
+ <div className="hidden md:flex items-center gap-6">
69
+ {navItems.map((item) => (
70
+ <Link key={item.href} href={item.href} className="text-sm font-medium transition-colors hover:text-primary">
71
+ {item.label}
72
+ </Link>
73
+ ))}
74
+
75
+ {user ? (
76
+ <DropdownMenu>
77
+ <DropdownMenuTrigger asChild>
78
+ <Button variant="ghost" className="relative h-8 w-8 rounded-full focus-visible:ring-0 focus-visible:ring-offset-0 hover:bg-transparent">
79
+ <Avatar className="h-8 w-8 border hover:border-primary transition-colors">
80
+ <AvatarFallback className="bg-primary text-primary-foreground">
81
+ {user?.name ? user.name.slice(0, 2).toUpperCase() : 'U'}
82
+ </AvatarFallback>
83
+ </Avatar>
84
+ </Button>
85
+ </DropdownMenuTrigger>
86
+ <DropdownMenuContent align="end" className="w-56">
87
+ <div className="px-4 py-3 border-b">
88
+ <div className="font-medium">{user?.name}</div>
89
+ <div className="text-xs text-muted-foreground">{user?.email}</div>
90
+ </div>
91
+ <DropdownMenuItem asChild>
92
+ <Link href="/dashboard">
93
+ <LayoutDashboard className="mr-2 h-4 w-4" />
94
+ Dashboard
95
+ </Link>
96
+ </DropdownMenuItem>
97
+ <DropdownMenuItem asChild>
98
+ <Link href="/profile">
99
+ <User className="mr-2 h-4 w-4" />
100
+ Profile
101
+ </Link>
102
+ </DropdownMenuItem>
103
+ <DropdownMenuItem onClick={handleLogout} className="text-destructive">
104
+ <LogOut className="mr-2 h-4 w-4" />
105
+ Logout
106
+ </DropdownMenuItem>
107
+ </DropdownMenuContent>
108
+ </DropdownMenu>
109
+ ) : (
110
+ <div className="flex items-center gap-2">
111
+ <Button asChild variant="ghost">
112
+ <Link href="/login">Login</Link>
113
+ </Button>
114
+ <Button asChild className="bg-primary hover:bg-primary/90">
115
+ <Link href="/register">Register</Link>
116
+ </Button>
117
+ </div>
118
+ )}
119
+ </div>
120
+
121
+ {/* Mobile Navigation */}
122
+ <div className="md:hidden flex items-center">
123
+ <Button variant="ghost" size="icon" onClick={() => setIsMenuOpen(!isMenuOpen)}>
124
+ {isMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
125
+ </Button>
126
+ </div>
127
+ </div>
128
+
129
+ {isMenuOpen && (
130
+ <div className="md:hidden border-t bg-background p-4 animate-in fade-in slide-in-from-top-2">
131
+ <div className="flex flex-col gap-4">
132
+ {navItems.map((item) => (
133
+ <Link
134
+ key={item.href}
135
+ href={item.href}
136
+ onClick={() => setIsMenuOpen(false)}
137
+ className="text-sm font-medium p-2 hover:bg-muted rounded-md"
138
+ >
139
+ {item.label}
140
+ </Link>
141
+ ))}
142
+
143
+ {user ? (
144
+ <>
145
+ <Link
146
+ href="/dashboard"
147
+ onClick={() => setIsMenuOpen(false)}
148
+ className="text-sm font-medium p-2 hover:bg-muted rounded-md flex items-center gap-2"
149
+ >
150
+ <LayoutDashboard className="h-4 w-4" />
151
+ Dashboard
152
+ </Link>
153
+ <Link
154
+ href="/profile"
155
+ onClick={() => setIsMenuOpen(false)}
156
+ className="text-sm font-medium p-2 hover:bg-muted rounded-md flex items-center gap-2"
157
+ >
158
+ <User className="h-4 w-4" />
159
+ Profile
160
+ </Link>
161
+ <Button
162
+ onClick={() => {
163
+ setIsMenuOpen(false)
164
+ handleLogout()
165
+ }}
166
+ variant="destructive"
167
+ className="w-full"
168
+ >
169
+ <LogOut className="mr-2 h-4 w-4" />
170
+ Logout
171
+ </Button>
172
+ </>
173
+ ) : (
174
+ <>
175
+ <Button asChild variant="outline" className="w-full">
176
+ <Link href="/login" onClick={() => setIsMenuOpen(false)}>
177
+ Login
178
+ </Link>
179
+ </Button>
180
+ <Button asChild className="w-full bg-primary">
181
+ <Link href="/register" onClick={() => setIsMenuOpen(false)}>
182
+ Register
183
+ </Link>
184
+ </Button>
185
+ </>
186
+ )}
187
+ </div>
188
+ </div>
189
+ )}
190
+ </nav>
191
+ )
192
+ }
Frontend/components/theme-provider.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ ThemeProvider as NextThemesProvider,
6
+ type ThemeProviderProps,
7
+ } from 'next-themes'
8
+
9
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
10
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
11
+ }
Frontend/components/ui/accordion.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as AccordionPrimitive from '@radix-ui/react-accordion'
5
+ import { ChevronDownIcon } from 'lucide-react'
6
+
7
+ import { cn } from '@/lib/utils'
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn('border-b last:border-b-0', className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ 'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
39
+ className,
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ )
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn('pt-0 pb-4', className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ )
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
Frontend/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { buttonVariants } from '@/components/ui/button'
8
+
9
+ function AlertDialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
12
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
13
+ }
14
+
15
+ function AlertDialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
18
+ return (
19
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20
+ )
21
+ }
22
+
23
+ function AlertDialogPortal({
24
+ ...props
25
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
26
+ return (
27
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
28
+ )
29
+ }
30
+
31
+ function AlertDialogOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
35
+ return (
36
+ <AlertDialogPrimitive.Overlay
37
+ data-slot="alert-dialog-overlay"
38
+ className={cn(
39
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ function AlertDialogContent({
48
+ className,
49
+ ...props
50
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
51
+ return (
52
+ <AlertDialogPortal>
53
+ <AlertDialogOverlay />
54
+ <AlertDialogPrimitive.Content
55
+ data-slot="alert-dialog-content"
56
+ className={cn(
57
+ 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
58
+ className,
59
+ )}
60
+ {...props}
61
+ />
62
+ </AlertDialogPortal>
63
+ )
64
+ }
65
+
66
+ function AlertDialogHeader({
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<'div'>) {
70
+ return (
71
+ <div
72
+ data-slot="alert-dialog-header"
73
+ className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
74
+ {...props}
75
+ />
76
+ )
77
+ }
78
+
79
+ function AlertDialogFooter({
80
+ className,
81
+ ...props
82
+ }: React.ComponentProps<'div'>) {
83
+ return (
84
+ <div
85
+ data-slot="alert-dialog-footer"
86
+ className={cn(
87
+ 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function AlertDialogTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
99
+ return (
100
+ <AlertDialogPrimitive.Title
101
+ data-slot="alert-dialog-title"
102
+ className={cn('text-lg font-semibold', className)}
103
+ {...props}
104
+ />
105
+ )
106
+ }
107
+
108
+ function AlertDialogDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
112
+ return (
113
+ <AlertDialogPrimitive.Description
114
+ data-slot="alert-dialog-description"
115
+ className={cn('text-muted-foreground text-sm', className)}
116
+ {...props}
117
+ />
118
+ )
119
+ }
120
+
121
+ function AlertDialogAction({
122
+ className,
123
+ ...props
124
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
125
+ return (
126
+ <AlertDialogPrimitive.Action
127
+ className={cn(buttonVariants(), className)}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ function AlertDialogCancel({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
137
+ return (
138
+ <AlertDialogPrimitive.Cancel
139
+ className={cn(buttonVariants({ variant: 'outline' }), className)}
140
+ {...props}
141
+ />
142
+ )
143
+ }
144
+
145
+ export {
146
+ AlertDialog,
147
+ AlertDialogPortal,
148
+ AlertDialogOverlay,
149
+ AlertDialogTrigger,
150
+ AlertDialogContent,
151
+ AlertDialogHeader,
152
+ AlertDialogFooter,
153
+ AlertDialogTitle,
154
+ AlertDialogDescription,
155
+ AlertDialogAction,
156
+ AlertDialogCancel,
157
+ }