devjhawar commited on
Commit
aa76de3
·
verified ·
1 Parent(s): 5b7955a

Upload folder using huggingface_hub

Browse files
.gitattributes ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git LFS tracking for binary files
2
+ # HuggingFace Spaces rejects binary files unless tracked via LFS
3
+ *.png filter=lfs diff=lfs merge=lfs -text
4
+ *.jpg filter=lfs diff=lfs merge=lfs -text
5
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
6
+ *.gif filter=lfs diff=lfs merge=lfs -text
7
+ *.svg filter=lfs diff=lfs merge=lfs -text
8
+ *.ico filter=lfs diff=lfs merge=lfs -text
9
+ *.zip filter=lfs diff=lfs merge=lfs -text
10
+ *.tar.gz filter=lfs diff=lfs merge=lfs -text
11
+ *.woff filter=lfs diff=lfs merge=lfs -text
12
+ *.woff2 filter=lfs diff=lfs merge=lfs -text
13
+ *.ttf filter=lfs diff=lfs merge=lfs -text
14
+ *.eot filter=lfs diff=lfs merge=lfs -text
15
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
16
+ *.webm filter=lfs diff=lfs merge=lfs -text
17
+ *.pdf filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
Binary files a/.gitignore and b/.gitignore differ
 
Dockerfile CHANGED
@@ -20,7 +20,7 @@ RUN pip install --no-cache-dir --timeout 300 --upgrade pip && \
20
  # This path is inside WORKDIR and persists into the running container
21
  ENV HF_HOME=/app/model_cache
22
  ENV SENTENCE_TRANSFORMERS_HOME=/app/model_cache/sentence_transformers
23
- RUN python -c "from sentence_transformers import SentenceTransformer; print('Downloading model...'); SentenceTransformer('BAAI/bge-large-en-v1.5', device='cpu'); print('Model cached.')"
24
 
25
  # Copy entire project
26
  COPY . .
 
20
  # This path is inside WORKDIR and persists into the running container
21
  ENV HF_HOME=/app/model_cache
22
  ENV SENTENCE_TRANSFORMERS_HOME=/app/model_cache/sentence_transformers
23
+ RUN python -c "from sentence_transformers import SentenceTransformer; print('Downloading model...'); SentenceTransformer('BAAI/bge-base-en-v1.5', device='cpu'); print('Model cached.')"
24
 
25
  # Copy entire project
26
  COPY . .
api/main.py CHANGED
@@ -2,15 +2,20 @@
2
 
3
  import sys
4
  import io
5
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
6
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
 
 
 
 
 
 
7
 
8
  from contextlib import asynccontextmanager
9
  import logging
10
 
11
  from fastapi import FastAPI
12
  from fastapi.middleware.cors import CORSMiddleware
13
- from fastapi.responses import JSONResponse
14
  import uvicorn
15
 
16
  from api.routes.query import router as query_router
@@ -30,7 +35,8 @@ async def lifespan(app: FastAPI):
30
 
31
  app.state.query_service = QueryService()
32
  logger.info("QueryService loaded and ready")
33
- yield
 
34
 
35
 
36
  # ------------------------------------------------------------------ #
@@ -43,7 +49,7 @@ app = FastAPI(
43
  lifespan=lifespan,
44
  )
45
 
46
- # CORS — wide open for development
47
  app.add_middleware(
48
  CORSMiddleware,
49
  allow_origins=["*"],
@@ -52,15 +58,6 @@ app.add_middleware(
52
  allow_headers=["*"],
53
  )
54
 
55
-
56
- # ------------------------------------------------------------------ #
57
- # Root endpoint — required for HF Space health check
58
- # ------------------------------------------------------------------ #
59
- @app.get("/")
60
- async def root():
61
- return JSONResponse({"status": "ok", "message": "PolicyLens RAG API is running"})
62
-
63
-
64
  # ------------------------------------------------------------------ #
65
  # Routers
66
  # ------------------------------------------------------------------ #
@@ -68,7 +65,6 @@ app.include_router(query_router)
68
  app.include_router(ingest_router)
69
  app.include_router(health_router)
70
 
71
-
72
  # ------------------------------------------------------------------ #
73
  # Dev entry point
74
  # ------------------------------------------------------------------ #
 
2
 
3
  import sys
4
  import io
5
+
6
+ # Force UTF-8 encoding for stdout and stderr on Windows to prevent logging crashes
7
+ if sys.platform == "win32":
8
+ try:
9
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
10
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
11
+ except Exception:
12
+ pass # Fallback if buffer is not available
13
 
14
  from contextlib import asynccontextmanager
15
  import logging
16
 
17
  from fastapi import FastAPI
18
  from fastapi.middleware.cors import CORSMiddleware
 
19
  import uvicorn
20
 
21
  from api.routes.query import router as query_router
 
35
 
36
  app.state.query_service = QueryService()
37
  logger.info("QueryService loaded and ready")
38
+ yield # app is running
39
+ # shutdown cleanup (nothing required for now)
40
 
41
 
42
  # ------------------------------------------------------------------ #
 
49
  lifespan=lifespan,
50
  )
51
 
52
+ # CORS — wide open for development; backend team will restrict in prod
53
  app.add_middleware(
54
  CORSMiddleware,
55
  allow_origins=["*"],
 
58
  allow_headers=["*"],
59
  )
60
 
 
 
 
 
 
 
 
 
 
61
  # ------------------------------------------------------------------ #
62
  # Routers
63
  # ------------------------------------------------------------------ #
 
65
  app.include_router(ingest_router)
66
  app.include_router(health_router)
67
 
 
68
  # ------------------------------------------------------------------ #
69
  # Dev entry point
70
  # ------------------------------------------------------------------ #
backend/src/routes/ingest.js CHANGED
@@ -61,12 +61,27 @@ router.post('/ingest/upload', authMiddleware, upload.single('file'), async (req,
61
  form.append('policy_id', policy_id);
62
  form.append('overwrite', 'false');
63
 
64
- // fire and forgetPython runs ingestion in background
65
- axios.post(`${process.env.PYTHON_API_URL}/ingest/upload`, form, {
66
- headers: form.getHeaders(),
67
- }).catch(err => console.error('Python ingest error:', err.message));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- // 6. Return immediately
70
  res.json({
71
  status: 'processing',
72
  policy_id,
@@ -76,6 +91,7 @@ router.post('/ingest/upload', authMiddleware, upload.single('file'), async (req,
76
  });
77
 
78
  } catch (err) {
 
79
  res.status(500).json({ error: err.message });
80
  }
81
  });
 
61
  form.append('policy_id', policy_id);
62
  form.append('overwrite', 'false');
63
 
64
+ // Forward to Pythonwait for response to surface errors
65
+ console.log(`[ingest] Forwarding to Python: ${process.env.PYTHON_API_URL}/ingest/upload | policy_id=${policy_id}`);
66
+
67
+ try {
68
+ const pythonResp = await axios.post(`${process.env.PYTHON_API_URL}/ingest/upload`, form, {
69
+ headers: form.getHeaders(),
70
+ maxBodyLength: Infinity,
71
+ maxContentLength: Infinity,
72
+ });
73
+ console.log('[ingest] Python accepted upload:', pythonResp.data);
74
+ } catch (err) {
75
+ console.error('[ingest] Python ingest error:', err.message);
76
+ if (err.response) {
77
+ console.error('[ingest] Python response status:', err.response.status);
78
+ console.error('[ingest] Python response body:', JSON.stringify(err.response.data));
79
+ throw new Error(`Python API Error: ${err.response.status} - ${JSON.stringify(err.response.data)}`);
80
+ }
81
+ throw new Error(`Failed to reach Python API: ${err.message}`);
82
+ }
83
 
84
+ // 6. Return response
85
  res.json({
86
  status: 'processing',
87
  policy_id,
 
91
  });
92
 
93
  } catch (err) {
94
+ console.error('[ingest] Fatal error:', err.message);
95
  res.status(500).json({ error: err.message });
96
  }
97
  });
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>landingpage</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "landingpage",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@tailwindcss/vite": "^4.2.1",
14
+ "lucide-react": "^0.575.0",
15
+ "react": "^19.2.0",
16
+ "react-dom": "^19.2.0",
17
+ "tailwindcss": "^4.2.1"
18
+ },
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.39.1",
21
+ "@types/react": "^19.2.7",
22
+ "@types/react-dom": "^19.2.3",
23
+ "@vitejs/plugin-react": "^5.1.1",
24
+ "eslint": "^9.39.1",
25
+ "eslint-plugin-react-hooks": "^7.0.1",
26
+ "eslint-plugin-react-refresh": "^0.4.24",
27
+ "globals": "^16.5.0",
28
+ "vite": "^7.3.1"
29
+ }
30
+ }
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+ import Navbar from "./components/Navbar/Navbar";
3
+ import Hero from "./components/Hero/Hero";
4
+ import Footer from "./components/Footer/Footer";
5
+ import Login from "./components/Auth/Login";
6
+ import UploadModal from "./components/Dashboard/UploadModal";
7
+ import Dboard from "./components/Dashboard/Dboard"; // This is the new dashboard
8
+ import Chatbot from "./components/Dashboard/Chatbot"; // This is the AI chat
9
+
10
+ export default function App() {
11
+ const [isDark, setIsDark] = useState(false);
12
+ const [appState, setAppState] = useState('home');
13
+ const [uploadedFile, setUploadedFile] = useState(null);
14
+
15
+ // Debugging: Watch state changes in your browser console
16
+ useEffect(() => {
17
+ console.log("Current App State:", appState);
18
+ }, [appState]);
19
+
20
+ useEffect(() => {
21
+ if (isDark) {
22
+ document.documentElement.classList.add('dark');
23
+ } else {
24
+ document.documentElement.classList.remove('dark');
25
+ }
26
+ }, [isDark]);
27
+
28
+ const navigateTo = (newState) => {
29
+ window.history.pushState({ appState: newState }, '', `#${newState}`);
30
+ setAppState(newState);
31
+ };
32
+
33
+ useEffect(() => {
34
+ window.history.replaceState({ appState: 'home' }, '', '#home');
35
+ const handlePopState = (e) => {
36
+ const state = e.state?.appState || 'home';
37
+ setAppState(state);
38
+ };
39
+ window.addEventListener('popstate', handlePopState);
40
+ return () => window.removeEventListener('popstate', handlePopState);
41
+ }, []);
42
+
43
+ const toggleTheme = () => setIsDark(!isDark);
44
+
45
+ const handleUploadComplete = (file) => {
46
+ console.log("Upload finished! Routing to Dboard...");
47
+ setUploadedFile(file);
48
+ // Force the state to 'dboard'
49
+ navigateTo('dboard');
50
+ };
51
+
52
+ // 1. DASHBOARD ROUTE (The new UI)
53
+ if (appState === 'dboard' || appState === 'dashboard') {
54
+ return (
55
+ <Dboard
56
+ file={uploadedFile}
57
+ isDark={isDark}
58
+ toggleTheme={toggleTheme}
59
+ onLogout={() => {
60
+ setUploadedFile(null);
61
+ navigateTo('home');
62
+ }}
63
+ onTriggerUpload={() => navigateTo('upload')}
64
+ onOpenIris={() => navigateTo('chatbot')}
65
+ />
66
+ );
67
+ }
68
+
69
+ // 2. CHATBOT ROUTE (The IRIS chat)
70
+ if (appState === 'chatbot') {
71
+ return (
72
+ <div className="relative min-h-screen">
73
+ <button
74
+ onClick={() => navigateTo('dboard')}
75
+ style={{
76
+ position: 'absolute', top: '24px', left: '24px', zIndex: 50,
77
+ padding: '10px 20px', borderRadius: '12px', fontWeight: 500, cursor: 'pointer',
78
+ background: isDark ? '#1D1D24' : '#ffffff',
79
+ color: isDark ? '#f0f9ff' : '#111827',
80
+ border: `1px solid ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}`,
81
+ fontFamily: "'Outfit', sans-serif"
82
+ }}
83
+ >
84
+ ← Back to Dashboard
85
+ </button>
86
+ <Chatbot file={uploadedFile} isDark={isDark} />
87
+ </div>
88
+ );
89
+ }
90
+
91
+ // 3. LOGIN ROUTE
92
+ if (appState === 'login') {
93
+ return (
94
+ <Login
95
+ onLoginSuccess={() => navigateTo('upload')}
96
+ onNavigateSignup={() => {}}
97
+ onBack={() => navigateTo('home')}
98
+ />
99
+ );
100
+ }
101
+
102
+ // 4. HOME & UPLOAD ROUTES
103
+ return (
104
+ <div className="min-h-screen flex flex-col font-sans bg-gray-50 dark:bg-[#0c0908] transition-colors duration-300">
105
+ <Navbar
106
+ isDark={isDark}
107
+ toggleTheme={toggleTheme}
108
+ hideNavLinks={appState !== 'home'}
109
+ onHome={() => navigateTo('home')}
110
+ />
111
+
112
+ <main className="flex-grow flex flex-col relative pt-20">
113
+ {appState === 'home' && (
114
+ <Hero onGetStarted={() => navigateTo('login')} />
115
+ )}
116
+ {appState === 'upload' && (
117
+ <UploadModal
118
+ onUploadComplete={handleUploadComplete}
119
+ onCancel={() => navigateTo('home')}
120
+ />
121
+ )}
122
+ </main>
123
+
124
+ {appState === 'home' && <Footer />}
125
+ </div>
126
+ );
127
+ }
frontend/src/assets/pla.png ADDED

Git LFS Details

  • SHA256: 6950a2a0e4abd0e70c84f6b6574f57133e297e5789329bf25f981c5c5bb6ac27
  • Pointer size: 131 Bytes
  • Size of remote file: 211 kB
frontend/src/assets/react.svg ADDED
frontend/src/components/About/About.jsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const cards = [
2
+ {
3
+ icon: '🧠',
4
+ title: 'Intelligent Parsing',
5
+ desc: 'Understands dense legal and policy language across formats — PDFs, DOCX, and scanned documents.',
6
+ },
7
+ {
8
+ icon: '⚡',
9
+ title: 'Instant Summaries',
10
+ desc: 'Receive concise, structured breakdowns the moment a document is uploaded. Zero waiting.',
11
+ },
12
+ {
13
+ icon: '🔍',
14
+ title: 'Clause Extraction',
15
+ desc: 'Pinpoints key clauses, obligations, deadlines, and risk terms automatically.',
16
+ },
17
+ {
18
+ icon: '🛡️',
19
+ title: 'Compliance Guard',
20
+ desc: 'Flags potential regulatory conflicts and compliance gaps before they become liabilities.',
21
+ },
22
+ ]
23
+
24
+ export default function About() {
25
+ return (
26
+ <section id="about" className="py-28 bg-black relative overflow-hidden">
27
+ <div className="absolute inset-0 pointer-events-none">
28
+ <div className="absolute top-0 right-0 w-150 h-100 bg-purple-950/40 rounded-full blur-[140px]" />
29
+ </div>
30
+
31
+ <div className="max-w-7xl mx-auto px-5 sm:px-8 lg:px-12 relative z-10">
32
+ <div className="text-center mb-16">
33
+ <span className="inline-block bg-purple-950/70 border border-purple-700/40 text-purple-400 text-xs font-bold px-4 py-1.5 rounded-full uppercase tracking-widest mb-5">
34
+ What We Do
35
+ </span>
36
+ <h2 className="text-4xl sm:text-5xl font-black text-white mb-5 leading-tight">
37
+ Built for People Who{' '}
38
+ <span className="text-purple-400">Can't Afford</span>
39
+ <br />to Miss the Fine Print
40
+ </h2>
41
+ <p className="text-gray-500 text-base sm:text-lg max-w-xl mx-auto leading-relaxed">
42
+ PolicyLens AI is purpose-built for professionals who work with high-stakes documents every day.
43
+ </p>
44
+ </div>
45
+
46
+ <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-5">
47
+ {cards.map(({ icon, title, desc }) => (
48
+ <div
49
+ key={title}
50
+ className="group bg-gray-950 border border-purple-900/30 hover:border-purple-600/60 rounded-2xl p-6 transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl hover:shadow-purple-900/30 cursor-default"
51
+ >
52
+ <div className="w-12 h-12 bg-purple-950 rounded-xl flex items-center justify-center text-2xl mb-5 border border-purple-800/40 group-hover:border-purple-600/60 transition-colors duration-300">
53
+ {icon}
54
+ </div>
55
+ <h3 className="text-white font-bold text-base mb-2">{title}</h3>
56
+ <p className="text-gray-500 text-sm leading-relaxed">{desc}</p>
57
+ <div className="mt-5 h-px bg-linear-to-r from-purple-600/0 via-purple-600/60 to-purple-600/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
58
+ </div>
59
+ ))}
60
+ </div>
61
+
62
+ {/* Audience strip */}
63
+ <div className="mt-16 grid grid-cols-2 sm:grid-cols-4 gap-4">
64
+ {[
65
+ ['🎓', 'Students'],
66
+ ['🔬', 'Researchers'],
67
+ ['⚖️', 'Legal Teams'],
68
+ ['🏛️', 'Policymakers'],
69
+ ].map(([icon, label]) => (
70
+ <div key={label} className="flex items-center gap-3 bg-gray-950/80 border border-purple-900/30 rounded-xl px-4 py-3">
71
+ <span className="text-xl">{icon}</span>
72
+ <span className="text-gray-300 font-semibold text-sm">{label}</span>
73
+ </div>
74
+ ))}
75
+ </div>
76
+ </div>
77
+ </section>
78
+ )
79
+ }
frontend/src/components/Auth/Login.jsx ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Eye, EyeOff, ArrowRight, Sun, Moon, Cpu, Shield, Zap, Lock } from 'lucide-react';
3
+
4
+ const FONT_LINK =
5
+ 'https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Syne:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600&display=swap';
6
+
7
+ const KEYFRAMES = `
8
+ @keyframes lp-up { from{opacity:0;transform:translateY(18px)} to{opacity:1;transform:none} }
9
+ @keyframes lp-left { from{opacity:0;transform:translateX(-18px)} to{opacity:1;transform:none} }
10
+ @keyframes lp-glow { 0%,100%{opacity:.35;transform:scale(1)} 50%{opacity:.7;transform:scale(1.06)} }
11
+ @keyframes lp-sway { 0%,100%{transform:rotate(-3deg) scale(1)} 50%{transform:rotate(3deg) scale(1.02)} }
12
+ @keyframes lp-twinkle{ 0%,100%{opacity:.1;transform:scale(.7)} 50%{opacity:.85;transform:scale(1.3)} }
13
+ @keyframes lp-orbit { from{transform:rotate(0deg) translateX(160px) rotate(0deg)} to{transform:rotate(360deg) translateX(160px) rotate(-360deg)} }
14
+ @keyframes lp-shake { 0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-5px)} 40%,80%{transform:translateX(5px)} }
15
+ @keyframes lp-spin { to{transform:rotate(360deg)} }
16
+ @keyframes lp-pulse { 0%,100%{box-shadow:0 0 0 0 currentColor} 50%{box-shadow:0 0 0 4px transparent} }
17
+
18
+ .lp-form { animation: lp-up .5s cubic-bezier(.16,1,.3,1) forwards }
19
+ .lp-panel { animation: lp-left .5s cubic-bezier(.16,1,.3,1) forwards }
20
+ .lp-shake { animation: lp-shake .4s ease }
21
+ .lp-spin { animation: lp-spin 1s linear infinite }
22
+ `;
23
+
24
+ const STARS = Array.from({ length: 70 }, (_, i) => ({
25
+ x: ((i * 137.508) % 100).toFixed(2),
26
+ y: ((i * 97.3) % 100).toFixed(2),
27
+ r: (0.5 + (i % 4) * 0.45).toFixed(1),
28
+ d: ((i * 0.22) % 3 ).toFixed(2),
29
+ t: (2 + (i % 5) * 0.7 ).toFixed(1),
30
+ }));
31
+
32
+ /* ── THEMES — matched to Hero.jsx palette ──────────────────── */
33
+ const LIGHT = {
34
+ pageBg: '#ffffff',
35
+ panelBg: '#111827', /* dark panel for contrast, gray-900 matches hero light text */
36
+ panelText: '#f9fafb',
37
+ panelSub: 'rgba(249,250,251,.5)',
38
+ panelAccent: '#0d9488', /* brand-teal */
39
+ panelGrad: 'linear-gradient(135deg,#0d9488,#14b8a6)',
40
+
41
+ cardBg: '#ffffff',
42
+ cardBorder: 'rgba(209,213,219,.6)',
43
+ cardShadow: '0 20px 60px rgba(0,0,0,.07)',
44
+
45
+ t1: '#111827', /* gray-900 */
46
+ t2: '#6b7280', /* gray-500 */
47
+ t3: '#9ca3af', /* gray-400 */
48
+ t4: 'rgba(209,213,219,.4)',
49
+
50
+ acc: '#0d9488',
51
+ acc2: '#14b8a6',
52
+ accGrad: 'linear-gradient(135deg,#0d9488,#14b8a6)',
53
+ pillBg: 'rgba(204,251,241,.5)',
54
+ pillBorder: 'rgba(153,246,228,.8)',
55
+ pillText: '#0f766e',
56
+
57
+ inputBg: '#f9fafb',
58
+ inputBorder: '#e5e7eb', /* gray-200 */
59
+ inputFocus: '#0d9488',
60
+ inputText: '#111827',
61
+ inputPH: '#9ca3af',
62
+ inputIcon: '#9ca3af',
63
+
64
+ labelColor: '#6b7280',
65
+ linkColor: '#0d9488',
66
+
67
+ btnBg: 'linear-gradient(135deg,#0d9488,#14b8a6)',
68
+ btnShadow: '0 6px 24px rgba(13,148,136,.3)',
69
+ btnText: '#ffffff',
70
+
71
+ dividerBg: 'rgba(209,213,219,.6)',
72
+ dividerText: '#9ca3af',
73
+
74
+ socialBg: '#f9fafb',
75
+ socialBorder:'#e5e7eb',
76
+ socialText: '#6b7280',
77
+
78
+ footerText: '#d1d5db',
79
+ toggleBg: '#f3f4f6',
80
+ toggleBorder:'#e5e7eb',
81
+
82
+ errorBg: '#fff5f5',
83
+ errorBorder: '#fecaca',
84
+ errorText: '#dc2626',
85
+ };
86
+
87
+ const DARK = {
88
+ pageBg: '#0c0908', /* hero dark navbar bg */
89
+ panelBg: '#080605', /* hero dark footer bg */
90
+ panelText: '#ecfeff', /* cyan-50 */
91
+ panelSub: 'rgba(207,250,254,.45)',
92
+ panelAccent: '#22d3ee', /* cyan-400 */
93
+ panelGrad: 'linear-gradient(135deg,#0e7490,#22d3ee)',
94
+
95
+ cardBg: '#15100d', /* hero dark card/modal bg */
96
+ cardBorder: 'rgba(21,94,117,.3)', /* cyan-900 */
97
+ cardShadow: '0 20px 60px rgba(0,0,0,.7)',
98
+
99
+ t1: '#ecfeff', /* cyan-50 */
100
+ t2: 'rgba(207,250,254,.6)',
101
+ t3: 'rgba(207,250,254,.35)',
102
+ t4: 'rgba(21,94,117,.2)',
103
+
104
+ acc: '#22d3ee', /* cyan-400 */
105
+ acc2: '#a5f3fc', /* cyan-200 */
106
+ accGrad: 'linear-gradient(135deg,#0e7490,#22d3ee)',
107
+ pillBg: 'rgba(21,94,117,.18)',
108
+ pillBorder: 'rgba(21,94,117,.4)',
109
+ pillText: '#a5f3fc',
110
+
111
+ inputBg: '#080605',
112
+ inputBorder: 'rgba(35,26,21,.8)', /* hero dark border #231a15 */
113
+ inputFocus: '#22d3ee',
114
+ inputText: '#ecfeff',
115
+ inputPH: 'rgba(207,250,254,.25)',
116
+ inputIcon: 'rgba(34,211,238,.4)',
117
+
118
+ labelColor: '#a5f3fc', /* cyan-200 */
119
+ linkColor: '#67e8f9', /* cyan-300 */
120
+
121
+ btnBg: '#e0f2fe', /* hero dark button bg */
122
+ btnShadow: '0 6px 24px rgba(34,211,238,.2)',
123
+ btnText: '#0c0908', /* hero dark button text */
124
+
125
+ dividerBg: 'rgba(35,26,21,.8)',
126
+ dividerText: 'rgba(207,250,254,.35)',
127
+
128
+ socialBg: 'rgba(21,94,117,.08)',
129
+ socialBorder:'rgba(35,26,21,.8)',
130
+ socialText: '#a5f3fc',
131
+
132
+ footerText: 'rgba(21,94,117,.5)',
133
+ toggleBg: '#1a1310', /* hero dark button bg */
134
+ toggleBorder:'#2a1f1a', /* hero dark border */
135
+
136
+ errorBg: 'rgba(239,68,68,.08)',
137
+ errorBorder: 'rgba(239,68,68,.25)',
138
+ errorText: '#fca5a5',
139
+ };
140
+
141
+ const GoogleIcon = () => (
142
+ <svg width="16" height="16" viewBox="0 0 24 24">
143
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
144
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
145
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"/>
146
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
147
+ </svg>
148
+ );
149
+
150
+ export default function LoginPage({ onLoginSuccess, onNavigateSignup, onBack }) {
151
+ const [dark, setDark] = useState(false);
152
+ const [email, setEmail] = useState('');
153
+ const [password, setPassword] = useState('');
154
+ const [showPw, setShowPw] = useState(false);
155
+ const [loading, setLoading] = useState(false);
156
+ const [error, setError] = useState('');
157
+ const [shake, setShake] = useState(false);
158
+ const T = dark ? DARK : LIGHT;
159
+
160
+ useEffect(() => {
161
+ const a = Object.assign(document.createElement('link'), { rel:'stylesheet', href:FONT_LINK });
162
+ const b = Object.assign(document.createElement('style'), { textContent:KEYFRAMES });
163
+ document.head.append(a, b);
164
+ return () => { a.remove(); b.remove(); };
165
+ }, []);
166
+
167
+ const triggerError = (msg) => {
168
+ setError(msg);
169
+ setShake(true);
170
+ setTimeout(() => setShake(false), 450);
171
+ };
172
+
173
+ const handleSubmit = async (e) => {
174
+ e.preventDefault();
175
+ if (!email.trim()) return triggerError('Please enter your email.');
176
+ if (!password.trim()) return triggerError('Please enter your password.');
177
+ setError('');
178
+ setLoading(true);
179
+ try {
180
+ /* ── connect your backend here ──────────────────────────
181
+ const res = await fetch('/api/auth/login', {
182
+ method: 'POST',
183
+ headers: { 'Content-Type': 'application/json' },
184
+ body: JSON.stringify({ email, password }),
185
+ });
186
+ if (!res.ok) throw new Error((await res.json()).message || 'Login failed');
187
+ const { token } = await res.json();
188
+ localStorage.setItem('token', token);
189
+ ─────────────────────────────────────────────────────── */
190
+ await new Promise(r => setTimeout(r, 1400));
191
+ onLoginSuccess?.({ email });
192
+ } catch (err) {
193
+ triggerError(err.message || 'Invalid credentials. Please try again.');
194
+ } finally {
195
+ setLoading(false);
196
+ }
197
+ };
198
+
199
+ const f = { fontFamily:"'DM Sans',sans-serif" };
200
+ const mono = { fontFamily:"'JetBrains Mono',monospace" };
201
+ const bebas = { fontFamily:"'Bebas Neue',cursive" };
202
+ const syne = { fontFamily:"'Syne',sans-serif" };
203
+
204
+ return (
205
+ <div style={{ ...f, minHeight:'100vh', background:T.pageBg, transition:'background .4s', display:'flex', alignItems:'center', justifyContent:'center', padding:16 }}>
206
+
207
+ {/* ── theme toggle (top right) ── */}
208
+ <button onClick={() => setDark(v => !v)} style={{
209
+ position:'fixed', top:20, right:20, zIndex:99,
210
+ width:38, height:38, borderRadius:11, cursor:'pointer',
211
+ display:'flex', alignItems:'center', justifyContent:'center',
212
+ background:T.toggleBg, border:`1px solid ${T.toggleBorder}`,
213
+ transition:'all .2s',
214
+ }}
215
+ onMouseEnter={e => { e.currentTarget.style.transform='rotate(14deg) scale(1.1)'; }}
216
+ onMouseLeave={e => { e.currentTarget.style.transform='none'; }}>
217
+ {dark
218
+ ? <Sun size={15} style={{ color:'#22d3ee' }}/>
219
+ : <Moon size={15} style={{ color:T.acc }}/>
220
+ }
221
+ </button>
222
+
223
+ {/* ══ CARD ════════════════════════════════════════════════ */}
224
+ <div style={{
225
+ display:'flex', overflow:'hidden',
226
+ width:'min(920px, 100%)', minHeight:580,
227
+ borderRadius:28,
228
+ background:T.cardBg, border:`1px solid ${T.cardBorder}`,
229
+ boxShadow:T.cardShadow,
230
+ transition:'background .4s, border-color .4s, box-shadow .4s',
231
+ }}>
232
+
233
+ {/* ── LEFT DECORATIVE PANEL ────────────────────────────── */}
234
+ <div className="lp-panel" style={{
235
+ width:380, flexShrink:0, position:'relative', overflow:'hidden',
236
+ background: T.panelBg,
237
+ display:'flex', flexDirection:'column', justifyContent:'space-between',
238
+ padding:40,
239
+ }}>
240
+
241
+ {dark ? (
242
+ /* DARK: Hero-style starfield + cyan glow */
243
+ <div style={{ position:'absolute', inset:0, pointerEvents:'none' }}>
244
+ <svg style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}>
245
+ {STARS.map((s,i) => (
246
+ <circle key={i} cx={`${s.x}%`} cy={`${s.y}%`} r={s.r} fill="#a5f3fc"
247
+ style={{ animation:`lp-twinkle ${s.t}s ease-in-out infinite`, animationDelay:`${s.d}s`, opacity:.12 }}/>
248
+ ))}
249
+ </svg>
250
+ {/* top cyan glow — matches hero dark atmosphere */}
251
+ <div style={{ position:'absolute', width:480, height:480, borderRadius:'50%',
252
+ top:'30%', left:'50%', transform:'translate(-50%,-50%)', filter:'blur(90px)',
253
+ background:'radial-gradient(circle,rgba(34,211,238,.12) 0%,transparent 65%)',
254
+ animation:'lp-glow 6s ease-in-out infinite' }}/>
255
+ {/* orbiting dot */}
256
+ <div style={{ position:'absolute', top:'50%', left:'50%', width:0, height:0 }}>
257
+ <div style={{ position:'absolute', width:4, height:4, borderRadius:'50%',
258
+ background:'#22d3ee', boxShadow:'0 0 8px #22d3ee',
259
+ marginLeft:-2, marginTop:-2,
260
+ animation:'lp-orbit 14s linear infinite', opacity:.5 }}/>
261
+ </div>
262
+ {/* subtle grid */}
263
+ <svg style={{ position:'absolute', inset:0, width:'100%', height:'100%', opacity:.03 }}>
264
+ <defs><pattern id="lpg" x="0" y="0" width="50" height="50" patternUnits="userSpaceOnUse">
265
+ <path d="M50 0L0 0 0 50" fill="none" stroke="#22d3ee" strokeWidth=".6"/>
266
+ </pattern></defs>
267
+ <rect width="100%" height="100%" fill="url(#lpg)"/>
268
+ </svg>
269
+ </div>
270
+ ) : (
271
+ /* LIGHT: Hero-style teal glow + dot grid */
272
+ <div style={{ position:'absolute', inset:0, pointerEvents:'none' }}>
273
+ {/* teal leaf shapes */}
274
+ <svg style={{ position:'absolute', top:'-8%', right:'-10%', width:280, height:280,
275
+ animation:'lp-sway 8s ease-in-out infinite', opacity:.07 }}>
276
+ <ellipse cx="140" cy="140" rx="120" ry="55" fill="#14b8a6" transform="rotate(-28 140 140)"/>
277
+ <ellipse cx="140" cy="140" rx="90" ry="38" fill="#0d9488" transform="rotate(12 140 140)"/>
278
+ </svg>
279
+ <svg style={{ position:'absolute', bottom:'-10%', left:'-8%', width:220, height:220,
280
+ animation:'lp-sway 11s ease-in-out infinite', animationDelay:'2s', opacity:.06 }}>
281
+ <ellipse cx="110" cy="110" rx="95" ry="42" fill="#14b8a6" transform="rotate(22 110 110)"/>
282
+ </svg>
283
+ {/* teal glow */}
284
+ <div style={{ position:'absolute', width:400, height:400, borderRadius:'50%',
285
+ top:'50%', left:'50%', transform:'translate(-50%,-50%)', filter:'blur(80px)',
286
+ background:'radial-gradient(circle,rgba(13,148,136,.12) 0%,transparent 65%)',
287
+ animation:'lp-glow 7s ease-in-out infinite' }}/>
288
+ {/* dot grid */}
289
+ <svg style={{ position:'absolute', inset:0, width:'100%', height:'100%', opacity:.06 }}>
290
+ <defs><pattern id="lpd" x="0" y="0" width="22" height="22" patternUnits="userSpaceOnUse">
291
+ <circle cx="1" cy="1" r=".9" fill="#0d9488"/>
292
+ </pattern></defs>
293
+ <rect width="100%" height="100%" fill="url(#lpd)"/>
294
+ </svg>
295
+ </div>
296
+ )}
297
+
298
+ {/* brand */}
299
+ <div style={{ position:'relative', zIndex:2 }}>
300
+ <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:40 }}>
301
+ <div style={{ width:40, height:40, borderRadius:12,
302
+ background:T.panelGrad, display:'flex', alignItems:'center', justifyContent:'center',
303
+ boxShadow:`0 4px 16px ${T.panelAccent}55` }}>
304
+ <Cpu size={18} color="#fff"/>
305
+ </div>
306
+ <span style={{ ...bebas, fontSize:22, letterSpacing:3, color:T.panelText }}>PolicyLens</span>
307
+ </div>
308
+
309
+ <h2 style={{ ...bebas, fontSize:44, letterSpacing:2, lineHeight:1.05,
310
+ color:T.panelText, margin:'0 0 16px' }}>
311
+ Your Legal<br/>Intelligence<br/>Platform
312
+ </h2>
313
+ <p style={{ fontSize:13, lineHeight:1.7, color:T.panelSub, maxWidth:260 }}>
314
+ Upload contracts and policies. Our AI scans every clause, flags every risk, and answers every question.
315
+ </p>
316
+ </div>
317
+
318
+ {/* feature bullets */}
319
+ <div style={{ position:'relative', zIndex:2, display:'flex', flexDirection:'column', gap:12 }}>
320
+ {[
321
+ { icon: Shield, text:'End-to-end encrypted analysis' },
322
+ { icon: Zap, text:'Risk detection in under 30 seconds' },
323
+ { icon: Lock, text:'Zero data retention policy' },
324
+ ].map(({ icon: Icon, text }) => (
325
+ <div key={text} style={{ display:'flex', alignItems:'center', gap:10 }}>
326
+ <div style={{ width:28, height:28, borderRadius:8, flexShrink:0,
327
+ display:'flex', alignItems:'center', justifyContent:'center',
328
+ background: dark ? 'rgba(34,211,238,.1)' : 'rgba(13,148,136,.15)',
329
+ border:`1px solid ${dark ? 'rgba(34,211,238,.2)' : 'rgba(13,148,136,.25)'}` }}>
330
+ <Icon size={13} style={{ color:T.panelAccent }}/>
331
+ </div>
332
+ <span style={{ fontSize:12, color:T.panelSub }}>{text}</span>
333
+ </div>
334
+ ))}
335
+ </div>
336
+ </div>
337
+
338
+ {/* ── RIGHT: LOGIN FORM ─────────────────────────────────── */}
339
+ <div style={{ flex:1, display:'flex', flexDirection:'column',
340
+ justifyContent:'center', padding:'48px 52px', position:'relative' }}>
341
+
342
+ <div className="lp-form">
343
+
344
+ {/* header */}
345
+ <div style={{ marginBottom:36 }}>
346
+ <span style={{
347
+ ...mono, display:'inline-block', marginBottom:12,
348
+ fontSize:9, letterSpacing:'3.5px', padding:'3px 12px', borderRadius:99,
349
+ background:T.pillBg, border:`1px solid ${T.pillBorder}`, color:T.pillText,
350
+ }}>SECURE LOGIN</span>
351
+ <h1 style={{ ...bebas, fontSize:40, letterSpacing:2, lineHeight:1,
352
+ color:T.t1, margin:'0 0 8px', transition:'color .4s' }}>
353
+ Welcome Back
354
+ </h1>
355
+ <p style={{ fontSize:13, color:T.t2, margin:0 }}>
356
+ Sign in to continue to your dashboard.
357
+ </p>
358
+ </div>
359
+
360
+ {/* error */}
361
+ {error && (
362
+ <div className={shake ? 'lp-shake' : ''} style={{
363
+ marginBottom:20, padding:'10px 14px', borderRadius:10, fontSize:12,
364
+ background:T.errorBg, border:`1px solid ${T.errorBorder}`, color:T.errorText,
365
+ display:'flex', alignItems:'center', gap:8,
366
+ }}>
367
+ <span style={{ fontSize:15 }}>⚠</span> {error}
368
+ </div>
369
+ )}
370
+
371
+ {/* form */}
372
+ <form onSubmit={handleSubmit} style={{ display:'flex', flexDirection:'column', gap:16 }}>
373
+
374
+ {/* email */}
375
+ <div>
376
+ <label style={{ ...f, display:'block', fontSize:11, fontWeight:500,
377
+ letterSpacing:1, marginBottom:6, color:T.labelColor, ...mono }}>
378
+ EMAIL ADDRESS
379
+ </label>
380
+ <div style={{ position:'relative' }}>
381
+ <input
382
+ type="email" value={email} onChange={e => setEmail(e.target.value)}
383
+ placeholder="you@company.com" autoComplete="email"
384
+ style={{
385
+ ...f, width:'100%', fontSize:13, padding:'11px 14px',
386
+ borderRadius:12, outline:'none', boxSizing:'border-box',
387
+ background:T.inputBg, border:`1px solid ${T.inputBorder}`,
388
+ color:T.inputText, transition:'border-color .2s, background .4s',
389
+ }}
390
+ onFocus={e => e.target.style.borderColor=T.inputFocus}
391
+ onBlur={e => e.target.style.borderColor=T.inputBorder}
392
+ />
393
+ </div>
394
+ </div>
395
+
396
+ {/* password */}
397
+ <div>
398
+ <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:6 }}>
399
+ <label style={{ ...f, display:'block', fontSize:11, fontWeight:500,
400
+ letterSpacing:1, color:T.labelColor, ...mono }}>
401
+ PASSWORD
402
+ </label>
403
+ <button type="button"
404
+ style={{ ...f, fontSize:11, color:T.linkColor, background:'none',
405
+ border:'none', cursor:'pointer', padding:0, transition:'opacity .15s' }}
406
+ onMouseEnter={e => e.currentTarget.style.opacity='.7'}
407
+ onMouseLeave={e => e.currentTarget.style.opacity='1'}>
408
+ Forgot password?
409
+ </button>
410
+ </div>
411
+ <div style={{ position:'relative' }}>
412
+ <input
413
+ type={showPw ? 'text' : 'password'} value={password}
414
+ onChange={e => setPassword(e.target.value)}
415
+ placeholder="••••••••••" autoComplete="current-password"
416
+ style={{
417
+ ...f, width:'100%', fontSize:13, padding:'11px 44px 11px 14px',
418
+ borderRadius:12, outline:'none', boxSizing:'border-box',
419
+ background:T.inputBg, border:`1px solid ${T.inputBorder}`,
420
+ color:T.inputText, transition:'border-color .2s, background .4s',
421
+ }}
422
+ onFocus={e => e.target.style.borderColor=T.inputFocus}
423
+ onBlur={e => e.target.style.borderColor=T.inputBorder}
424
+ />
425
+ <button type="button" onClick={() => setShowPw(v => !v)}
426
+ style={{ position:'absolute', right:12, top:'50%', transform:'translateY(-50%)',
427
+ background:'none', border:'none', cursor:'pointer', padding:0,
428
+ color:T.inputIcon, display:'flex', alignItems:'center' }}>
429
+ {showPw ? <EyeOff size={15}/> : <Eye size={15}/>}
430
+ </button>
431
+ </div>
432
+ </div>
433
+
434
+ {/* remember me */}
435
+ <label style={{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', marginTop:-4 }}>
436
+ <input type="checkbox" style={{ accentColor:T.acc, width:14, height:14 }}/>
437
+ <span style={{ fontSize:12, color:T.t2 }}>Remember me for 30 days</span>
438
+ </label>
439
+
440
+ {/* submit */}
441
+ <button type="submit" disabled={loading}
442
+ style={{
443
+ ...syne, width:'100%', padding:'13px', borderRadius:13, border:'none',
444
+ fontSize:14, fontWeight:600, cursor: loading ? 'not-allowed' : 'pointer',
445
+ background: loading ? T.t4 : T.btnBg,
446
+ color: T.btnText,
447
+ boxShadow: loading ? 'none' : T.btnShadow,
448
+ display:'flex', alignItems:'center', justifyContent:'center', gap:8,
449
+ transition:'all .2s', opacity: loading ? .7 : 1, marginTop:4,
450
+ }}
451
+ onMouseEnter={e => { if (!loading) e.currentTarget.style.transform='translateY(-1px)'; }}
452
+ onMouseLeave={e => e.currentTarget.style.transform='none'}>
453
+ {loading
454
+ ? <><span className="lp-spin" style={{ display:'inline-block', width:16, height:16,
455
+ border:`2px solid rgba(255,255,255,.3)`, borderTopColor:'#fff', borderRadius:'50%' }}/> Signing in…</>
456
+ : <>Sign In <ArrowRight size={15}/></>
457
+ }
458
+ </button>
459
+ </form>
460
+
461
+ {/* divider */}
462
+ <div style={{ display:'flex', alignItems:'center', gap:12, margin:'22px 0' }}>
463
+ <div style={{ flex:1, height:1, background:T.dividerBg }}/>
464
+ <span style={{ ...mono, fontSize:10, letterSpacing:2, color:T.dividerText }}>OR</span>
465
+ <div style={{ flex:1, height:1, background:T.dividerBg }}/>
466
+ </div>
467
+
468
+ {/* Google SSO */}
469
+ <button type="button" style={{
470
+ ...f, width:'100%', padding:'12px', borderRadius:13, cursor:'pointer',
471
+ display:'flex', alignItems:'center', justifyContent:'center', gap:10,
472
+ background:T.socialBg, border:`1px solid ${T.socialBorder}`, color:T.socialText,
473
+ fontSize:13, fontWeight:500, transition:'all .15s',
474
+ }}
475
+ onMouseEnter={e => { e.currentTarget.style.borderColor=T.acc; e.currentTarget.style.transform='translateY(-1px)'; }}
476
+ onMouseLeave={e => { e.currentTarget.style.borderColor=T.socialBorder; e.currentTarget.style.transform='none'; }}>
477
+ <GoogleIcon/> Continue with Google
478
+ </button>
479
+
480
+ {/* signup link */}
481
+ <p style={{ marginTop:24, textAlign:'center', fontSize:12, color:T.t2 }}>
482
+ Don't have an account?{' '}
483
+ <button type="button" onClick={onNavigateSignup}
484
+ style={{ ...f, fontSize:12, fontWeight:600, color:T.linkColor,
485
+ background:'none', border:'none', cursor:'pointer', padding:0 }}>
486
+ Create one free →
487
+ </button>
488
+ </p>
489
+
490
+ {/* footer */}
491
+ <p style={{ ...mono, marginTop:20, textAlign:'center', fontSize:10,
492
+ letterSpacing:.5, color:T.footerText }}>
493
+ Secured by PolicyLens · v2.4.1 · SOC 2 Compliant
494
+ </p>
495
+
496
+ </div>
497
+ </div>
498
+ </div>
499
+ </div>
500
+ );
501
+ }
frontend/src/components/Dashboard/Chatbot.jsx ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ MessageSquare, Send, Download, User, Loader2,
3
+ Sparkles, ShieldCheck, Sun, Moon, Aperture
4
+ } from 'lucide-react';
5
+ import { useState, useRef, useEffect } from 'react';
6
+
7
+ const FONT_LINK =
8
+ 'https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Syne:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600&display=swap';
9
+
10
+ const KEYFRAMES = `
11
+ @keyframes cb-slideUp { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
12
+ @keyframes cb-db { 0%,80%,100%{transform:translateY(0)} 40%{transform:translateY(-5px)} }
13
+ @keyframes cb-pulse { 0%,100%{opacity:.4;transform:scale(1)} 50%{opacity:1;transform:scale(1.2)} }
14
+ .cb-db1{animation:cb-db 1.2s infinite 0ms}
15
+ .cb-db2{animation:cb-db 1.2s infinite 150ms}
16
+ .cb-db3{animation:cb-db 1.2s infinite 300ms}
17
+ .cb-msg{animation:cb-slideUp .25s cubic-bezier(0.16, 1, 0.3, 1)}
18
+ .cb-scrollbar::-webkit-scrollbar{width:5px}
19
+ .cb-scrollbar::-webkit-scrollbar-thumb{border-radius:5px}
20
+ .cb-noscroll::-webkit-scrollbar{display:none}
21
+ `;
22
+
23
+ const LIGHT = {
24
+ pageBg: '#ffffff',
25
+ cardBg: '#ffffff',
26
+ cardBorder: '#e5e7eb',
27
+ cardShadow: '0 12px 32px -4px rgba(0,0,0,.07)',
28
+ t1: '#111827',
29
+ t2: '#374151',
30
+ t3: '#6b7280',
31
+ acc: '#0d9488',
32
+ accHover: '#0f766e',
33
+ accGrad: 'linear-gradient(135deg,#0d9488,#14b8a6)',
34
+ headerBg: '#f9fafb',
35
+ headerBorder: '#e5e7eb',
36
+ msgAreaBg: '#ffffff',
37
+ msgUserBg: 'linear-gradient(135deg,#0d9488,#14b8a6)',
38
+ msgUserShadow: '0 4px 14px rgba(13,148,136,.25)',
39
+ msgUserText: '#ffffff',
40
+ msgAiBg: '#f3f4f6',
41
+ msgAiBorder: 'transparent',
42
+ msgAiText: '#111827',
43
+ inputBg: '#f9fafb',
44
+ inputBorder: '#e5e7eb',
45
+ inputFocus: '#0d9488',
46
+ inputText: '#111827',
47
+ sendActive: 'linear-gradient(135deg,#0d9488,#14b8a6)',
48
+ sendDisabled: '#e5e7eb',
49
+ statusDot: '#10b981',
50
+ statusGlow: '0 0 6px #10b981',
51
+ onlineText: '#6b7280',
52
+ scrollThumb: '#d1d5db',
53
+ };
54
+
55
+ const DARK = {
56
+ pageBg: '#0c0908',
57
+ cardBg: '#15100d',
58
+ cardBorder: '#2a1f1a',
59
+ cardShadow: '0 12px 40px -4px rgba(0,0,0,.6)',
60
+ t1: '#ecfeff',
61
+ t2: 'rgba(207,250,254,.7)',
62
+ t3: 'rgba(207,250,254,.4)',
63
+ acc: '#22d3ee',
64
+ accHover: '#67e8f9',
65
+ accGrad: 'linear-gradient(135deg,#0e7490,#22d3ee)',
66
+ headerBg: '#080605',
67
+ headerBorder: '#231a15',
68
+ msgAreaBg: '#15100d',
69
+ msgUserBg: '#e0f2fe',
70
+ msgUserShadow: '0 4px 14px rgba(34,211,238,.15)',
71
+ msgUserText: '#0c0908',
72
+ msgAiBg: '#1a1310',
73
+ msgAiBorder: 'transparent',
74
+ msgAiText: '#ecfeff',
75
+ inputBg: '#080605',
76
+ inputBorder: '#231a15',
77
+ inputFocus: '#22d3ee',
78
+ inputText: '#ecfeff',
79
+ sendActive: '#e0f2fe',
80
+ sendDisabled: '#1a1310',
81
+ statusDot: '#10b981',
82
+ statusGlow: '0 0 6px rgba(16,185,129,.4)',
83
+ onlineText: 'rgba(207,250,254,.4)',
84
+ scrollThumb: '#2a1f1a',
85
+ };
86
+
87
+ function MessageBubble({ msg, T }) {
88
+ const isUser = msg.sender === 'user';
89
+ return (
90
+ <div className="cb-msg" style={{ display:'flex', gap:12, width:'100%', justifyContent: isUser ? 'flex-end' : 'flex-start' }}>
91
+ {!isUser && (
92
+ <div style={{ width:36, height:36, borderRadius:12, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.msgAiBg, border:`1px solid ${T.cardBorder}` }}>
93
+ <Aperture size={18} style={{ color:T.acc }}/>
94
+ </div>
95
+ )}
96
+ <div style={{ maxWidth:'75%', padding:'14px 20px', fontSize:15, lineHeight:1.6, borderRadius: isUser ? '20px 4px 20px 20px' : '4px 20px 20px 20px', background: isUser ? T.msgUserBg : T.msgAiBg, color: isUser ? T.msgUserText : T.msgAiText, boxShadow: isUser ? T.msgUserShadow : 'none', fontFamily:"'DM Sans', sans-serif", fontWeight:400, transition:'background .4s, color .4s' }}>
97
+ {msg.text}
98
+ </div>
99
+ {isUser && (
100
+ <div style={{ width:36, height:36, borderRadius:12, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.msgAiBg, border:`1px solid ${T.cardBorder}` }}>
101
+ <User size={18} style={{ color:T.t2 }}/>
102
+ </div>
103
+ )}
104
+ </div>
105
+ );
106
+ }
107
+
108
+ export default function Chatbot({ file, isDark: initDark }) {
109
+ const [chatMessage, setChatMessage] = useState('');
110
+ const [isTyping, setIsTyping] = useState(false);
111
+ const [dark, setDark] = useState(false);
112
+
113
+ const [messages, setMessages] = useState([
114
+ { id:1, sender:'ai', text: file
115
+ ? `Hello, I'm IRIS. I've processed "${file.name}". I am ready to answer any questions regarding its clauses, liabilities, or summaries.`
116
+ : "Hello, I'm IRIS. The document is processed, and I am ready to answer any questions regarding its clauses, liabilities, or summaries." },
117
+ ]);
118
+
119
+ const messagesEndRef = useRef(null);
120
+ const T = dark ? DARK : LIGHT;
121
+
122
+ useEffect(() => {
123
+ const link = Object.assign(document.createElement('link'), { rel:'stylesheet', href:FONT_LINK });
124
+ const style = Object.assign(document.createElement('style'), { textContent:KEYFRAMES });
125
+ document.head.append(link, style);
126
+ return () => { link.remove(); style.remove(); };
127
+ }, []);
128
+
129
+ useEffect(() => {
130
+ messagesEndRef.current?.scrollIntoView({ behavior:'smooth' });
131
+ }, [messages, isTyping]);
132
+
133
+ const handleSend = (e) => {
134
+ if (e?.preventDefault) e.preventDefault();
135
+ const text = typeof e === 'string' ? e : chatMessage;
136
+ if (!text.trim()) return;
137
+ setMessages(p => [...p, { id:Date.now(), sender:'user', text }]);
138
+ setChatMessage('');
139
+ setIsTyping(true);
140
+ setTimeout(() => {
141
+ setMessages(p => [...p, { id:Date.now()+1, sender:'ai',
142
+ text:'Analyzing clause context… This is a simulated response. Connect the backend model to retrieve clause-specific analysis from your document.' }]);
143
+ setIsTyping(false);
144
+ }, 1600);
145
+ };
146
+
147
+ /* ── Export chat log as .pdf ── */
148
+ const handleExport = async () => {
149
+ // Dynamically load jsPDF
150
+ if (!window.jspdf) {
151
+ await new Promise((resolve, reject) => {
152
+ const s = document.createElement('script');
153
+ s.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
154
+ s.onload = resolve; s.onerror = reject;
155
+ document.head.appendChild(s);
156
+ });
157
+ }
158
+ const { jsPDF } = window.jspdf;
159
+ const doc = new jsPDF({ unit:'pt', format:'a4' });
160
+
161
+ const pageW = doc.internal.pageSize.getWidth();
162
+ const pageH = doc.internal.pageSize.getHeight();
163
+ const margin = 48;
164
+ const maxW = pageW - margin * 2;
165
+ let y = margin;
166
+
167
+ const addPage = () => { doc.addPage(); y = margin; };
168
+ const checkY = (h) => { if (y + h > pageH - margin) addPage(); };
169
+
170
+ // Header
171
+ doc.setFillColor(13, 148, 136);
172
+ doc.rect(0, 0, pageW, 6, 'F');
173
+
174
+ doc.setFont('helvetica', 'bold');
175
+ doc.setFontSize(28);
176
+ doc.setTextColor(17, 24, 39);
177
+ doc.text('IRIS', margin, y + 28); y += 44;
178
+
179
+ doc.setFont('helvetica', 'normal');
180
+ doc.setFontSize(10);
181
+ doc.setTextColor(107, 114, 128);
182
+ doc.text(`Document: ${file?.name || 'Unknown'}`, margin, y);
183
+ doc.text(`Exported: ${new Date().toLocaleString()}`, margin, y + 14);
184
+ y += 36;
185
+
186
+ // Divider
187
+ doc.setDrawColor(229, 231, 235);
188
+ doc.setLineWidth(0.5);
189
+ doc.line(margin, y, pageW - margin, y);
190
+ y += 24;
191
+
192
+ // Messages
193
+ messages.forEach((m) => {
194
+ const isUser = m.sender === 'user';
195
+ const label = isUser ? 'You' : 'IRIS';
196
+ const lines = doc.splitTextToSize(m.text, maxW - 16);
197
+ const bubbleH = lines.length * 14 + 28;
198
+
199
+ checkY(bubbleH + 20);
200
+
201
+ // Bubble background
202
+ if (isUser) {
203
+ doc.setFillColor(13, 148, 136);
204
+ doc.roundedRect(margin, y, maxW, bubbleH, 8, 8, 'F');
205
+ } else {
206
+ doc.setFillColor(243, 244, 246);
207
+ doc.roundedRect(margin, y, maxW, bubbleH, 8, 8, 'F');
208
+ }
209
+
210
+ // Label
211
+ doc.setFont('helvetica', 'bold');
212
+ doc.setFontSize(9);
213
+ doc.setTextColor(isUser ? 255 : 107, isUser ? 255 : 114, isUser ? 255 : 128);
214
+ doc.text(label, margin + 12, y + 16);
215
+
216
+ // Message text
217
+ doc.setFont('helvetica', 'normal');
218
+ doc.setFontSize(11);
219
+ doc.setTextColor(isUser ? 255 : 17, isUser ? 255 : 24, isUser ? 255 : 39);
220
+ doc.text(lines, margin + 12, y + 28);
221
+
222
+ y += bubbleH + 12;
223
+ });
224
+
225
+ // Footer
226
+ checkY(32);
227
+ doc.setDrawColor(229, 231, 235);
228
+ doc.line(margin, y, pageW - margin, y);
229
+ y += 16;
230
+ doc.setFont('helvetica', 'normal');
231
+ doc.setFontSize(9);
232
+ doc.setTextColor(156, 163, 175);
233
+ doc.text('IRIS insights are for guidance only • Not legal advice', margin, y);
234
+
235
+ doc.save(`IRIS_log_${Date.now()}.pdf`);
236
+ };
237
+
238
+ const canSend = chatMessage.trim() && !isTyping;
239
+ const f = { fontFamily:"'DM Sans', sans-serif" };
240
+ const syne = { fontFamily:"'Syne', sans-serif" };
241
+ const bebas = { fontFamily:"'Bebas Neue', cursive" };
242
+ const mono = { fontFamily:"'JetBrains Mono', monospace" };
243
+
244
+ return (
245
+ <div style={{ minHeight:'100vh', display:'flex', flexDirection:'column', alignItems:'center', padding:'40px 16px', background:T.pageBg, color:T.t1, fontFamily:"'DM Sans', sans-serif", transition:'background .4s, color .4s' }}>
246
+
247
+ {/* HEADER */}
248
+ <div style={{ width:'100%', maxWidth:900, display:'flex', justifyContent:'space-between', alignItems:'flex-end', marginBottom:28 }}>
249
+ <div>
250
+ <span style={{ display:'inline-flex', alignItems:'center', gap:6, marginBottom:12, padding:'6px 14px', borderRadius:8, fontSize:11, letterSpacing:1.5, ...mono, fontWeight:500, textTransform:'uppercase', background:T.msgAiBg, border:`1px solid ${T.cardBorder}`, color:T.acc, transition:'all .4s' }}>
251
+ <ShieldCheck size={14} /> IRIS Secure Session
252
+ </span>
253
+ <h1 style={{ fontSize:56, lineHeight:1, margin:0, color:T.t1, ...bebas, letterSpacing:3, transition:'color .4s' }}>
254
+ IRIS
255
+ </h1>
256
+ <p style={{ ...f, marginTop:8, fontSize:14, color:T.t3, display:'flex', alignItems:'center', gap:8 }}>
257
+ <Sparkles size={16} style={{ color:T.acc }}/>
258
+ {file ? `Currently analyzing: ${file.name}` : 'Document context fully loaded and verified.'}
259
+ </p>
260
+ </div>
261
+
262
+ <div style={{ display:'flex', alignItems:'center', gap:12 }}>
263
+ <button onClick={() => setDark(v => !v)} style={{ width:44, height:44, borderRadius:12, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', background:T.cardBg, border:`1px solid ${T.cardBorder}`, boxShadow:T.cardShadow, transition:'all .2s' }}
264
+ onMouseEnter={e => { e.currentTarget.style.transform='rotate(14deg) scale(1.1)'; }}
265
+ onMouseLeave={e => { e.currentTarget.style.transform='none'; }}>
266
+ {dark ? <Sun size={18} style={{ color:'#22d3ee' }}/> : <Moon size={18} style={{ color:T.acc }}/>}
267
+ </button>
268
+ <button onClick={handleExport} style={{ display:'flex', alignItems:'center', gap:8, padding:'0 20px', height:44, fontSize:14, fontWeight:500, borderRadius:12, cursor:'pointer', ...f, background:T.cardBg, border:`1px solid ${T.cardBorder}`, color:T.t1, boxShadow:T.cardShadow, transition:'all .2s' }}
269
+ onMouseEnter={e => { e.currentTarget.style.borderColor=T.acc; e.currentTarget.style.color=T.acc; e.currentTarget.style.transform='scale(1.02)'; }}
270
+ onMouseLeave={e => { e.currentTarget.style.borderColor=T.cardBorder; e.currentTarget.style.color=T.t1; e.currentTarget.style.transform='none'; }}>
271
+ <Download size={16}/> Export Log
272
+ </button>
273
+ </div>
274
+ </div>
275
+
276
+ {/* CHAT CARD */}
277
+ <div style={{ width:'100%', maxWidth:900, flex:1, display:'flex', flexDirection:'column', borderRadius:24, overflow:'hidden', background:T.cardBg, border:`1px solid ${T.cardBorder}`, boxShadow:T.cardShadow, transition:'background .4s, border-color .4s, box-shadow .4s' }}>
278
+
279
+ {/* chat header */}
280
+ <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'18px 28px', background:T.headerBg, borderBottom:`1px solid ${T.headerBorder}`, transition:'background .4s, border-color .4s' }}>
281
+ <div style={{ display:'flex', alignItems:'center', gap:16 }}>
282
+ <div style={{ width:44, height:44, borderRadius:12, display:'flex', alignItems:'center', justifyContent:'center', background:T.accGrad, boxShadow:T.msgUserShadow }}>
283
+ <Aperture size={20} color={dark ? '#0c0908' : '#ffffff'} />
284
+ </div>
285
+ <div>
286
+ <p style={{ margin:0, fontSize:16, fontWeight:600, color:T.t1, ...syne }}>IRIS</p>
287
+ <div style={{ display:'flex', alignItems:'center', gap:8, marginTop:4 }}>
288
+ <span style={{ width:8, height:8, borderRadius:'50%', display:'inline-block', background:T.statusDot, boxShadow:T.statusGlow, animation:'cb-pulse 2s ease-in-out infinite' }}/>
289
+ <span style={{ ...mono, fontSize:11, color:T.onlineText, fontWeight:500 }}>System Online</span>
290
+ </div>
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ {/* messages */}
296
+ <div className="cb-scrollbar" style={{ flex:1, overflowY:'auto', padding:'32px', display:'flex', flexDirection:'column', gap:24, background:T.msgAreaBg, transition:'background .4s' }}>
297
+ {messages.map(m => <MessageBubble key={m.id} msg={m} T={T}/>)}
298
+ {isTyping && (
299
+ <div className="cb-msg" style={{ display:'flex', gap:12, alignItems:'center' }}>
300
+ <div style={{ width:36, height:36, borderRadius:12, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.msgAiBg, border:`1px solid ${T.cardBorder}` }}>
301
+ <Loader2 size={16} style={{ color:T.acc, animation:'spin 1s linear infinite' }}/>
302
+ </div>
303
+ <div style={{ padding:'16px 20px', display:'flex', gap:6, alignItems:'center', borderRadius:'4px 20px 20px 20px', background:T.msgAiBg }}>
304
+ <span className="cb-db1" style={{ width:6, height:6, borderRadius:'50%', background:T.acc, display:'inline-block' }}/>
305
+ <span className="cb-db2" style={{ width:6, height:6, borderRadius:'50%', background:T.acc, display:'inline-block' }}/>
306
+ <span className="cb-db3" style={{ width:6, height:6, borderRadius:'50%', background:T.acc, display:'inline-block' }}/>
307
+ </div>
308
+ </div>
309
+ )}
310
+ <div ref={messagesEndRef}/>
311
+ </div>
312
+
313
+ {/* input area */}
314
+ <div style={{ padding:'20px 28px 24px', background:T.headerBg, borderTop:`1px solid ${T.headerBorder}`, transition:'background .4s, border-color .4s' }}>
315
+ <div style={{ display:'flex', alignItems:'center', gap:14 }}>
316
+ <input
317
+ type="text" value={chatMessage}
318
+ onChange={e => setChatMessage(e.target.value)}
319
+ onKeyDown={e => { if (e.key==='Enter' && !e.shiftKey) handleSend(chatMessage); }}
320
+ placeholder="Ask IRIS anything about the document..."
321
+ style={{ flex:1, fontSize:15, padding:'16px 20px', borderRadius:16, outline:'none', background:T.inputBg, border:`1px solid ${T.inputBorder}`, color:T.inputText, ...f, transition:'border-color .2s, background .4s' }}
322
+ onFocus={e => e.target.style.borderColor=T.inputFocus}
323
+ onBlur={e => e.target.style.borderColor=T.inputBorder}
324
+ />
325
+ <button onClick={() => handleSend(chatMessage)} disabled={!canSend} style={{ flexShrink:0, width:52, height:52, borderRadius:16, display:'flex', alignItems:'center', justifyContent:'center', border:'none', cursor: canSend ? 'pointer' : 'not-allowed', background: canSend ? T.sendActive : T.sendDisabled, color: dark ? '#0c0908' : '#ffffff', opacity: canSend ? 1 : 0.5, boxShadow: canSend ? T.msgUserShadow : 'none', transition:'all .2s ease' }}
326
+ onMouseEnter={e => { if (canSend) e.currentTarget.style.transform='scale(1.05)'; }}
327
+ onMouseLeave={e => e.currentTarget.style.transform='none'}>
328
+ <Send size={20} />
329
+ </button>
330
+ </div>
331
+ <p style={{ ...mono, marginTop:18, textAlign:'center', fontSize:11, letterSpacing:1, color:T.t3, fontWeight:500 }}>
332
+ IRIS insights are for guidance • Not legal advice
333
+ </p>
334
+ </div>
335
+ </div>
336
+
337
+ <style>{`
338
+ @keyframes spin { to { transform: rotate(360deg); } }
339
+ .cb-scrollbar::-webkit-scrollbar-thumb { background: ${T.scrollThumb}; }
340
+ `}</style>
341
+ </div>
342
+ );
343
+ }
frontend/src/components/Dashboard/Dboard.jsx ADDED
@@ -0,0 +1,752 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/components/Dashboard/Dboard.jsx
2
+ import { useState, useRef, useEffect, useCallback } from 'react';
3
+ import {
4
+ Upload, Clock, X, FileText, AlertTriangle, CheckCircle2,
5
+ Shield, DollarSign, Calendar, Info, Trash2, Eye, FilePlus,
6
+ Home, Aperture, Sun, Moon, Menu, BadgeCheck, Banknote,
7
+ RefreshCw, Send, Loader2
8
+ } from 'lucide-react';
9
+
10
+ const FONT_LINK =
11
+ 'https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Syne:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600&display=swap';
12
+
13
+ const KEYFRAMES = `
14
+ @keyframes cb-slideUp { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
15
+ @keyframes cb-db { 0%,80%,100%{transform:translateY(0)} 40%{transform:translateY(-5px)} }
16
+ @keyframes cb-pulse { 0%,100%{opacity:.4;transform:scale(1)} 50%{opacity:1;transform:scale(1.2)} }
17
+ @keyframes spin { to { transform: rotate(360deg); } }
18
+ @keyframes fadeIn { from{opacity:0;transform:scale(.97)} to{opacity:1;transform:scale(1)} }
19
+ .cb-db1{animation:cb-db 1.2s infinite 0ms}
20
+ .cb-db2{animation:cb-db 1.2s infinite 150ms}
21
+ .cb-db3{animation:cb-db 1.2s infinite 300ms}
22
+ .cb-msg{animation:cb-slideUp .25s cubic-bezier(0.16,1,0.3,1)}
23
+ .spin-anim{animation:spin 1s linear infinite}
24
+ .fade-in{animation:fadeIn .3s cubic-bezier(0.16,1,0.3,1)}
25
+ .cb-scrollbar::-webkit-scrollbar{width:5px}
26
+ .cb-scrollbar::-webkit-scrollbar-thumb{border-radius:5px}
27
+ `;
28
+
29
+ const LIGHT = {
30
+ pageBg:'#ffffff', cardBg:'#ffffff', cardBorder:'#e5e7eb',
31
+ cardShadow:'0 12px 32px -4px rgba(0,0,0,.07)',
32
+ t1:'#111827', t2:'#374151', t3:'#6b7280',
33
+ acc:'#0d9488', accHover:'#0f766e',
34
+ accGrad:'linear-gradient(135deg,#0d9488,#14b8a6)',
35
+ headerBg:'#f9fafb', headerBorder:'#e5e7eb',
36
+ msgAreaBg:'#ffffff',
37
+ msgUserBg:'linear-gradient(135deg,#0d9488,#14b8a6)',
38
+ msgUserShadow:'0 4px 14px rgba(13,148,136,.25)',
39
+ msgUserText:'#ffffff', msgAiBg:'#f3f4f6', msgAiText:'#111827',
40
+ inputBg:'#f9fafb', inputBorder:'#e5e7eb', inputFocus:'#0d9488', inputText:'#111827',
41
+ sendActive:'linear-gradient(135deg,#0d9488,#14b8a6)', sendDisabled:'#e5e7eb',
42
+ statusDot:'#10b981', statusGlow:'0 0 6px #10b981', onlineText:'#6b7280',
43
+ scrollThumb:'#d1d5db',
44
+ sidebarBg:'#f9fafb',
45
+ navActiveBg:'rgba(13,148,136,0.1)', navActiveClr:'#0d9488', navActiveBrd:'rgba(13,148,136,0.2)',
46
+ navHoverBg:'#f3f4f6', accBg:'rgba(13,148,136,0.07)',
47
+ redBg:'rgba(239,68,68,0.07)', redClr:'#dc2626', redBrd:'rgba(239,68,68,0.18)',
48
+ greenBg:'rgba(16,185,129,0.07)', greenClr:'#059669', greenBrd:'rgba(16,185,129,0.18)',
49
+ amberBg:'rgba(245,158,11,0.07)', amberClr:'#d97706', amberBrd:'rgba(245,158,11,0.18)',
50
+ badgeBg:'rgba(13,148,136,0.09)', badgeClr:'#0d9488',
51
+ divider:'#f3f4f6',
52
+ };
53
+
54
+ const DARK = {
55
+ pageBg:'#0c0908', cardBg:'#15100d', cardBorder:'#2a1f1a',
56
+ cardShadow:'0 12px 40px -4px rgba(0,0,0,.6)',
57
+ t1:'#ecfeff', t2:'rgba(207,250,254,.7)', t3:'rgba(207,250,254,.4)',
58
+ acc:'#22d3ee', accHover:'#67e8f9',
59
+ accGrad:'linear-gradient(135deg,#0e7490,#22d3ee)',
60
+ headerBg:'#080605', headerBorder:'#231a15',
61
+ msgAreaBg:'#15100d',
62
+ msgUserBg:'#e0f2fe',
63
+ msgUserShadow:'0 4px 14px rgba(34,211,238,.15)',
64
+ msgUserText:'#0c0908', msgAiBg:'#1a1310', msgAiText:'#ecfeff',
65
+ inputBg:'#080605', inputBorder:'#231a15', inputFocus:'#22d3ee', inputText:'#ecfeff',
66
+ sendActive:'#e0f2fe', sendDisabled:'#1a1310',
67
+ statusDot:'#10b981', statusGlow:'0 0 6px rgba(16,185,129,.4)', onlineText:'rgba(207,250,254,.4)',
68
+ scrollThumb:'#2a1f1a',
69
+ sidebarBg:'#080605',
70
+ navActiveBg:'rgba(34,211,238,0.08)', navActiveClr:'#22d3ee', navActiveBrd:'rgba(34,211,238,0.2)',
71
+ navHoverBg:'rgba(255,255,255,0.03)', accBg:'rgba(34,211,238,0.07)',
72
+ redBg:'rgba(239,68,68,0.07)', redClr:'#f87171', redBrd:'rgba(239,68,68,0.18)',
73
+ greenBg:'rgba(16,185,129,0.07)', greenClr:'#34d399', greenBrd:'rgba(16,185,129,0.18)',
74
+ amberBg:'rgba(245,158,11,0.07)', amberClr:'#fbbf24', amberBrd:'rgba(245,158,11,0.18)',
75
+ badgeBg:'rgba(34,211,238,0.08)', badgeClr:'#22d3ee',
76
+ divider:'#1a1310',
77
+ };
78
+
79
+ const SAMPLE_HISTORY = [
80
+ {
81
+ id:1, filename:'HDFC_Life_ClickProtect.pdf', date:'Mar 15, 2026',
82
+ policy_type:'Term Life', insurer:'HDFC Life',
83
+ analysis:{
84
+ policy_name:'HDFC Life Click 2 Protect Super',
85
+ policy_type:'Term Life', insurer:'HDFC Life Insurance Co. Ltd.', uin:'101N145V02',
86
+ key_benefits:[
87
+ 'Life cover up to age 85','Critical illness benefit rider',
88
+ 'Premium waiver on disability','Return of premium option',
89
+ 'Increasing cover option','Accidental death benefit',
90
+ ],
91
+ exclusions:[
92
+ 'Suicide within 12 months of issuance','Pre-existing conditions (first 2 years)',
93
+ 'Hazardous activities or adventure sports','War, riot or civil commotion',
94
+ 'Self-inflicted injuries','Substance or alcohol abuse',
95
+ ],
96
+ death_benefit:'Sum assured paid as lump sum or monthly income to nominee. Minimum sum assured is ₹50 Lakhs.',
97
+ survival_benefit:'No survival benefit under base plan. Return of Premium variant returns total premiums paid on maturity.',
98
+ surrender_value:'Policy acquires surrender value after 3 consecutive premium years. Value depends on premiums paid and remaining term.',
99
+ loan_facility:null,
100
+ free_look_period:'30 days',
101
+ tax_benefit:'Premiums qualify for deduction under Section 80C up to ₹1.5L. Death benefit is fully tax-free under Section 10(10D).',
102
+ important_conditions:[
103
+ 'Medical examination mandatory for sum assured above ₹1 Crore',
104
+ 'Grace period of 30 days for annual/semi-annual mode',
105
+ 'Policy lapses if premium unpaid beyond grace period',
106
+ 'Revival allowed within 5 years of lapse with applicable interest',
107
+ ],
108
+ },
109
+ },
110
+ { id:2, filename:'LIC_JeevanAnand.pdf', date:'Mar 12, 2026', policy_type:'Endowment', insurer:'LIC of India', analysis:null },
111
+ { id:3, filename:'StarHealth_Comprehensive.pdf', date:'Mar 10, 2026', policy_type:'Health', insurer:'Star Health', analysis:null },
112
+ ];
113
+
114
+ export default function Dboard({ file, isDark: initDark = true }) {
115
+ const [dark, setDark] = useState(initDark);
116
+ const [sidebarOpen, setSidebarOpen] = useState(true);
117
+ const [activeTab, setActiveTab] = useState('home');
118
+ const [history, setHistory] = useState(SAMPLE_HISTORY);
119
+ const [activeAnalysis, setActiveAnalysis] = useState(null);
120
+ const [isDragging, setIsDragging] = useState(false);
121
+ const [uploading, setUploading] = useState(false);
122
+ const [uploadPct, setUploadPct] = useState(0);
123
+ const [chatOpen, setChatOpen] = useState(false);
124
+ const [chatInput, setChatInput] = useState('');
125
+ const [isTyping, setIsTyping] = useState(false);
126
+ const [chatMessages, setChatMessages] = useState([
127
+ {
128
+ id:1, sender:'ai',
129
+ text: file
130
+ ? `Hello, I'm IRIS. I've processed "${file.name}". Ask me anything about its clauses, benefits, or exclusions.`
131
+ : "Hello, I'm IRIS. Upload an insurance policy and I'll analyze every clause, benefit, and exclusion for you.",
132
+ },
133
+ ]);
134
+
135
+ const fileRef = useRef(null);
136
+ const hdrRef = useRef(null);
137
+ const chatEnd = useRef(null);
138
+
139
+ const T = dark ? DARK : LIGHT;
140
+ const f = { fontFamily:"'DM Sans', sans-serif" };
141
+ const syne = { fontFamily:"'Syne', sans-serif" };
142
+ const bbs = { fontFamily:"'Bebas Neue', cursive" };
143
+ const mono = { fontFamily:"'JetBrains Mono', monospace" };
144
+
145
+ useEffect(() => {
146
+ const link = Object.assign(document.createElement('link'), { rel:'stylesheet', href:FONT_LINK });
147
+ const style = Object.assign(document.createElement('style'), { textContent:KEYFRAMES });
148
+ document.head.append(link, style);
149
+ return () => { link.remove(); style.remove(); };
150
+ }, []);
151
+
152
+ useEffect(() => {
153
+ chatEnd.current?.scrollIntoView({ behavior:'smooth' });
154
+ }, [chatMessages, isTyping]);
155
+
156
+ useEffect(() => {
157
+ if (file) processFile(file);
158
+ // eslint-disable-next-line react-hooks/exhaustive-deps
159
+ }, []);
160
+
161
+ const processFile = useCallback((f) => {
162
+ setUploading(true); setUploadPct(0);
163
+ const iv = setInterval(() => {
164
+ setUploadPct(p => {
165
+ if (p >= 100) {
166
+ clearInterval(iv);
167
+ setUploading(false);
168
+ setActiveAnalysis(SAMPLE_HISTORY[0].analysis);
169
+ setHistory(prev => [{
170
+ id: Date.now(),
171
+ filename: f.name,
172
+ date: new Date().toLocaleDateString('en-IN', { day:'2-digit', month:'short', year:'numeric' }),
173
+ policy_type:'Term Life', insurer:'Detected',
174
+ analysis: SAMPLE_HISTORY[0].analysis,
175
+ }, ...prev]);
176
+ setActiveTab('home');
177
+ return 100;
178
+ }
179
+ return p + 4;
180
+ });
181
+ }, 80);
182
+ }, []);
183
+
184
+ const handleDrop = useCallback((e) => {
185
+ e.preventDefault(); setIsDragging(false);
186
+ const picked = e.dataTransfer.files[0];
187
+ if (picked) processFile(picked);
188
+ }, [processFile]);
189
+
190
+ const sendChat = () => {
191
+ if (!chatInput.trim() || isTyping) return;
192
+ const text = chatInput;
193
+ setChatMessages(p => [...p, { id:Date.now(), sender:'user', text }]);
194
+ setChatInput(''); setIsTyping(true);
195
+ setTimeout(() => {
196
+ setChatMessages(p => [...p, {
197
+ id:Date.now()+1, sender:'ai',
198
+ text:'Analyzing clause context… Connect the backend model to retrieve clause-specific analysis from your document.',
199
+ }]);
200
+ setIsTyping(false);
201
+ }, 1600);
202
+ };
203
+
204
+ // ── Sub-components ────────────────────────────────────────────────────────
205
+
206
+ const IrisAvatar = ({ size=40 }) => (
207
+ <div style={{
208
+ width:size, height:size, borderRadius:Math.round(size*0.28),
209
+ flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center',
210
+ background:T.accGrad, boxShadow:T.msgUserShadow,
211
+ }}>
212
+ <Aperture size={Math.round(size*0.48)} color={dark ? '#0c0908' : '#ffffff'} />
213
+ </div>
214
+ );
215
+
216
+ const NavBtn = ({ id, icon:Icon, label }) => {
217
+ const active = activeTab === id;
218
+ return (
219
+ <button
220
+ onClick={() => setActiveTab(id)}
221
+ style={{
222
+ width:'100%', display:'flex', alignItems:'center',
223
+ gap:12, padding: sidebarOpen ? '10px 14px' : '10px',
224
+ justifyContent: sidebarOpen ? 'flex-start' : 'center',
225
+ borderRadius:12, cursor:'pointer',
226
+ border:`1px solid ${active ? T.navActiveBrd : 'transparent'}`,
227
+ background: active ? T.navActiveBg : 'transparent',
228
+ color: active ? T.navActiveClr : T.t3,
229
+ ...f, fontSize:14, fontWeight:500, transition:'all .2s',
230
+ }}
231
+ onMouseEnter={e => { if (!active) e.currentTarget.style.background = T.navHoverBg; }}
232
+ onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
233
+ >
234
+ <Icon size={16} style={{ flexShrink:0 }} />
235
+ {sidebarOpen && <span>{label}</span>}
236
+ </button>
237
+ );
238
+ };
239
+
240
+ const Badge = ({ label, variant='acc' }) => {
241
+ const map = {
242
+ acc: [T.badgeBg, T.badgeClr],
243
+ green: [T.greenBg, T.greenClr],
244
+ red: [T.redBg, T.redClr ],
245
+ amber: [T.amberBg, T.amberClr],
246
+ };
247
+ const [bg, clr] = map[variant] || map.acc;
248
+ return (
249
+ <span style={{
250
+ display:'inline-flex', alignItems:'center',
251
+ padding:'2px 10px', borderRadius:20,
252
+ ...mono, fontSize:10, fontWeight:600, background:bg, color:clr,
253
+ }}>{label}</span>
254
+ );
255
+ };
256
+
257
+ const InfoCard = ({ icon:Icon, label, value, variant='acc' }) => {
258
+ const map = {
259
+ acc: [T.accBg, T.acc ],
260
+ green: [T.greenBg, T.greenClr],
261
+ amber: [T.amberBg, T.amberClr],
262
+ };
263
+ const [ibg, iclr] = map[variant] || map.acc;
264
+ return (
265
+ <div style={{ borderRadius:16, padding:'18px 20px', background:T.cardBg, border:`1px solid ${T.cardBorder}`, boxShadow:T.cardShadow }}>
266
+ <div style={{ display:'flex', alignItems:'flex-start', gap:12 }}>
267
+ <div style={{ width:36, height:36, borderRadius:10, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:ibg }}>
268
+ <Icon size={16} style={{ color:iclr }} />
269
+ </div>
270
+ <div style={{ minWidth:0 }}>
271
+ <p style={{ ...mono, fontSize:10, fontWeight:600, letterSpacing:1.2, textTransform:'uppercase', color:T.t3, margin:'0 0 6px 0' }}>{label}</p>
272
+ <p style={{ ...f, fontSize:13, lineHeight:1.65, color: value ? T.t2 : T.t3, margin:0, fontStyle: value ? 'normal' : 'italic' }}>
273
+ {value || 'Not applicable'}
274
+ </p>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ );
279
+ };
280
+
281
+ // ── Upload Zone ───────────────────────────────────────────────────────────
282
+ const UploadZone = () => (
283
+ <div
284
+ onDragOver={e => { e.preventDefault(); setIsDragging(true); }}
285
+ onDragLeave={() => setIsDragging(false)}
286
+ onDrop={handleDrop}
287
+ onClick={() => fileRef.current?.click()}
288
+ style={{
289
+ borderRadius:20, padding:'52px 24px',
290
+ border:`2px dashed ${isDragging ? T.acc : T.cardBorder}`,
291
+ background: isDragging ? T.accBg : T.headerBg,
292
+ cursor:'pointer', transition:'all .3s',
293
+ display:'flex', flexDirection:'column',
294
+ alignItems:'center', justifyContent:'center',
295
+ gap:20, textAlign:'center',
296
+ }}
297
+ >
298
+ <input ref={fileRef} type="file" accept=".pdf,.doc,.docx"
299
+ style={{ display:'none' }}
300
+ onChange={e => { if (e.target.files[0]) processFile(e.target.files[0]); }}
301
+ />
302
+ {uploading ? (
303
+ <div style={{ display:'flex', flexDirection:'column', alignItems:'center', gap:14, width:'100%' }}>
304
+ <div className="spin-anim" style={{ width:40, height:40, borderRadius:'50%', border:`3px solid ${T.cardBorder}`, borderTopColor:T.acc }} />
305
+ <p style={{ ...syne, fontSize:15, fontWeight:600, color:T.t1, margin:0 }}>Analyzing policy with IRIS…</p>
306
+ <div style={{ width:'100%', maxWidth:320, height:4, borderRadius:4, background:T.cardBorder }}>
307
+ <div style={{ height:'100%', borderRadius:4, background:T.accGrad, width:`${uploadPct}%`, transition:'width .15s' }} />
308
+ </div>
309
+ <p style={{ ...mono, fontSize:11, color:T.t3, margin:0 }}>{uploadPct}% — Running ML pipeline</p>
310
+ </div>
311
+ ) : (
312
+ <>
313
+ <div style={{ width:64, height:64, borderRadius:18, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.accBg, border:`1px solid ${T.cardBorder}` }}>
314
+ <Upload size={28} style={{ color:T.acc }} />
315
+ </div>
316
+ <div>
317
+ <p style={{ ...syne, fontSize:16, fontWeight:600, color:T.t1, margin:'0 0 4px 0' }}>Upload your insurance policy</p>
318
+ <p style={{ ...f, fontSize:13, color:T.t3, margin:0 }}>Drag & drop or click to browse · PDF, DOC up to 20MB</p>
319
+ </div>
320
+ <button style={{
321
+ display:'flex', alignItems:'center', gap:8, padding:'10px 26px',
322
+ borderRadius:12, border:'none', background:T.accGrad, cursor:'pointer',
323
+ color: dark ? '#0c0908' : '#ffffff',
324
+ ...syne, fontSize:14, fontWeight:600, boxShadow:T.msgUserShadow, marginTop:4,
325
+ }}>
326
+ <FilePlus size={16} /> Choose File
327
+ </button>
328
+ </>
329
+ )}
330
+ </div>
331
+ );
332
+
333
+ // ── Analysis renderer ─────────────────────────────────────────────────────
334
+ const renderAnalysis = (a) => (
335
+ <div className="fade-in" style={{ display:'flex', flexDirection:'column', gap:20 }}>
336
+
337
+ {/* Policy header */}
338
+ <div style={{ borderRadius:20, padding:'26px 30px', background:T.cardBg, border:`1px solid ${T.cardBorder}`, boxShadow:T.cardShadow }}>
339
+ <div style={{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', flexWrap:'wrap', gap:16 }}>
340
+ <div>
341
+ <h2 style={{ ...bbs, fontSize:42, letterSpacing:2, color:T.t1, margin:'0 0 6px 0', lineHeight:1 }}>{a.policy_name}</h2>
342
+ <p style={{ ...syne, fontSize:15, fontWeight:500, color:T.t2, margin:'0 0 4px 0' }}>{a.insurer}</p>
343
+ {a.uin && <p style={{ ...mono, fontSize:11, color:T.t3, margin:0 }}>UIN: {a.uin}</p>}
344
+ </div>
345
+ <div style={{ display:'flex', gap:8, flexWrap:'wrap', alignItems:'center' }}>
346
+ <Badge label={a.policy_type} variant="acc" />
347
+ <Badge label="✓ Analyzed" variant="green" />
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ {/* Benefits + Exclusions */}
353
+ <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:20 }}>
354
+ <div style={{ borderRadius:20, padding:'22px 24px', background:T.cardBg, border:`1px solid ${T.cardBorder}`, boxShadow:T.cardShadow }}>
355
+ <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:16 }}>
356
+ <div style={{ width:30, height:30, borderRadius:8, display:'flex', alignItems:'center', justifyContent:'center', background:T.greenBg }}>
357
+ <CheckCircle2 size={15} style={{ color:T.greenClr }} />
358
+ </div>
359
+ <p style={{ ...mono, fontSize:10, fontWeight:600, letterSpacing:1.2, textTransform:'uppercase', color:T.t3, margin:0 }}>Key Benefits</p>
360
+ </div>
361
+ <ul style={{ margin:0, padding:0, listStyle:'none', display:'flex', flexDirection:'column', gap:10 }}>
362
+ {a.key_benefits.map((b,i) => (
363
+ <li key={i} style={{ display:'flex', alignItems:'flex-start', gap:10 }}>
364
+ <span style={{ width:6, height:6, borderRadius:'50%', flexShrink:0, marginTop:6, background:T.greenClr, boxShadow:`0 0 6px ${T.greenClr}55` }} />
365
+ <span style={{ ...f, fontSize:13, color:T.t2, lineHeight:1.5 }}>{b}</span>
366
+ </li>
367
+ ))}
368
+ </ul>
369
+ </div>
370
+
371
+ <div style={{ borderRadius:20, padding:'22px 24px', background:T.cardBg, border:`1px solid ${T.cardBorder}`, boxShadow:T.cardShadow }}>
372
+ <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:16 }}>
373
+ <div style={{ width:30, height:30, borderRadius:8, display:'flex', alignItems:'center', justifyContent:'center', background:T.redBg }}>
374
+ <AlertTriangle size={15} style={{ color:T.redClr }} />
375
+ </div>
376
+ <p style={{ ...mono, fontSize:10, fontWeight:600, letterSpacing:1.2, textTransform:'uppercase', color:T.t3, margin:0 }}>Exclusions</p>
377
+ </div>
378
+ <ul style={{ margin:0, padding:0, listStyle:'none', display:'flex', flexDirection:'column', gap:10 }}>
379
+ {a.exclusions.map((e,i) => (
380
+ <li key={i} style={{ display:'flex', alignItems:'flex-start', gap:10 }}>
381
+ <span style={{ width:6, height:6, borderRadius:'50%', flexShrink:0, marginTop:6, background:T.redClr, boxShadow:`0 0 6px ${T.redClr}55` }} />
382
+ <span style={{ ...f, fontSize:13, color:T.t2, lineHeight:1.5 }}>{e}</span>
383
+ </li>
384
+ ))}
385
+ </ul>
386
+ </div>
387
+ </div>
388
+
389
+ {/* Info grid */}
390
+ <div style={{ display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:16 }}>
391
+ <InfoCard icon={Shield} label="Death Benefit" value={a.death_benefit} variant="acc" />
392
+ <InfoCard icon={BadgeCheck} label="Survival / Maturity" value={a.survival_benefit} variant="green" />
393
+ <InfoCard icon={RefreshCw} label="Surrender Value" value={a.surrender_value} variant="amber" />
394
+ <InfoCard icon={Banknote} label="Loan Facility" value={a.loan_facility} variant="acc" />
395
+ <InfoCard icon={Calendar} label="Free Look Period" value={a.free_look_period} variant="green" />
396
+ <InfoCard icon={DollarSign} label="Tax Benefits" value={a.tax_benefit} variant="amber" />
397
+ </div>
398
+
399
+ {/* Important conditions */}
400
+ <div style={{ borderRadius:20, padding:'22px 24px', background:T.cardBg, border:`1px solid ${T.cardBorder}`, boxShadow:T.cardShadow }}>
401
+ <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:18 }}>
402
+ <div style={{ width:30, height:30, borderRadius:8, display:'flex', alignItems:'center', justifyContent:'center', background:T.amberBg }}>
403
+ <Info size={15} style={{ color:T.amberClr }} />
404
+ </div>
405
+ <p style={{ ...mono, fontSize:10, fontWeight:600, letterSpacing:1.2, textTransform:'uppercase', color:T.t3, margin:0 }}>Important Conditions</p>
406
+ </div>
407
+ <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12 }}>
408
+ {a.important_conditions.map((c,i) => (
409
+ <div key={i} style={{ display:'flex', alignItems:'flex-start', gap:12, padding:'14px 16px', borderRadius:12, background:T.amberBg, border:`1px solid ${T.amberBrd}` }}>
410
+ <span style={{ ...mono, fontSize:11, fontWeight:700, color:T.amberClr, flexShrink:0, marginTop:1 }}>{String(i+1).padStart(2,'0')}</span>
411
+ <span style={{ ...f, fontSize:13, color:T.t2, lineHeight:1.55 }}>{c}</span>
412
+ </div>
413
+ ))}
414
+ </div>
415
+ </div>
416
+ </div>
417
+ );
418
+
419
+ // ── History renderer ──────────────────────────────────────────────────────
420
+ const renderHistory = () => (
421
+ <div className="fade-in" style={{ display:'flex', flexDirection:'column', gap:12 }}>
422
+ {history.length === 0 && (
423
+ <div style={{ textAlign:'center', padding:'60px 0', color:T.t3, ...f }}>No documents analyzed yet.</div>
424
+ )}
425
+ {history.map(item => (
426
+ <div key={item.id} style={{ display:'flex', alignItems:'center', gap:16, padding:'18px 22px', borderRadius:18, background:T.cardBg, border:`1px solid ${T.cardBorder}`, boxShadow:T.cardShadow }}>
427
+ <div style={{ width:44, height:44, borderRadius:12, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.accBg, border:`1px solid ${T.cardBorder}` }}>
428
+ <FileText size={20} style={{ color:T.acc }} />
429
+ </div>
430
+ <div style={{ flex:1, minWidth:0 }}>
431
+ <p style={{ ...syne, fontSize:14, fontWeight:600, color:T.t1, margin:'0 0 4px 0', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{item.filename}</p>
432
+ <div style={{ display:'flex', alignItems:'center', gap:8 }}>
433
+ <span style={{ ...mono, fontSize:11, color:T.t3 }}>{item.date}</span>
434
+ <span style={{ color:T.cardBorder }}>·</span>
435
+ <Badge label={item.policy_type} variant="acc" />
436
+ <span style={{ color:T.cardBorder }}>·</span>
437
+ <span style={{ ...f, fontSize:12, color:T.t3 }}>{item.insurer}</span>
438
+ </div>
439
+ </div>
440
+ <div style={{ display:'flex', gap:8 }}>
441
+ {item.analysis && (
442
+ <button
443
+ onClick={() => { setActiveAnalysis(item.analysis); setActiveTab('home'); }}
444
+ style={{ display:'flex', alignItems:'center', gap:6, padding:'7px 14px', borderRadius:10, cursor:'pointer', border:`1px solid ${T.navActiveBrd}`, background:T.navActiveBg, color:T.navActiveClr, ...f, fontSize:12, fontWeight:500, transition:'all .2s' }}
445
+ >
446
+ <Eye size={13} /> View
447
+ </button>
448
+ )}
449
+ <button
450
+ onClick={() => setHistory(h => h.filter(x => x.id !== item.id))}
451
+ style={{ width:34, height:34, borderRadius:10, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', background:'transparent', border:`1px solid ${T.cardBorder}`, color:T.t3, transition:'all .2s' }}
452
+ onMouseEnter={e => { e.currentTarget.style.borderColor=T.redClr; e.currentTarget.style.color=T.redClr; e.currentTarget.style.background=T.redBg; }}
453
+ onMouseLeave={e => { e.currentTarget.style.borderColor=T.cardBorder; e.currentTarget.style.color=T.t3; e.currentTarget.style.background='transparent'; }}
454
+ >
455
+ <Trash2 size={14} />
456
+ </button>
457
+ </div>
458
+ </div>
459
+ ))}
460
+ </div>
461
+ );
462
+
463
+ // ════════════════════════ MAIN RENDER ═════════════════════════════════════
464
+ return (
465
+ <div style={{ display:'flex', height:'100vh', overflow:'hidden', background:T.pageBg, color:T.t1, ...f, transition:'background .4s, color .4s' }}>
466
+
467
+ {/* ── SIDEBAR ── */}
468
+ <aside style={{
469
+ flexShrink:0, display:'flex', flexDirection:'column',
470
+ width: sidebarOpen ? 240 : 66,
471
+ background:T.sidebarBg, borderRight:`1px solid ${T.headerBorder}`,
472
+ transition:'width .3s cubic-bezier(0.16,1,0.3,1)', overflow:'hidden',
473
+ }}>
474
+
475
+ {/* Logo row — FIX 1: minWidth:240 REMOVED */}
476
+ <div style={{
477
+ display:'flex', alignItems:'center', gap:12,
478
+ padding:'20px 16px', borderBottom:`1px solid ${T.headerBorder}`,
479
+ // ✅ minWidth:240 removed — menu button now stays visible when collapsed
480
+ }}>
481
+ <IrisAvatar size={38} />
482
+ {sidebarOpen && (
483
+ <div>
484
+ <p style={{ ...bbs, fontSize:28, letterSpacing:2, color:T.t1, margin:0, lineHeight:1 }}>IRIS</p>
485
+ <p style={{ ...mono, fontSize:9, color:T.t3, margin:'2px 0 0 0', letterSpacing:1 }}>POLICY INTELLIGENCE</p>
486
+ </div>
487
+ )}
488
+ <button
489
+ onClick={() => setSidebarOpen(v => !v)}
490
+ style={{
491
+ marginLeft:'auto', width:30, height:30, borderRadius:8, cursor:'pointer',
492
+ display:'flex', alignItems:'center', justifyContent:'center',
493
+ background:'transparent', border:`1px solid ${T.cardBorder}`, color:T.t3,
494
+ transition:'all .2s', flexShrink:0,
495
+ }}
496
+ onMouseEnter={e => { e.currentTarget.style.borderColor=T.acc; e.currentTarget.style.color=T.acc; }}
497
+ onMouseLeave={e => { e.currentTarget.style.borderColor=T.cardBorder; e.currentTarget.style.color=T.t3; }}
498
+ >
499
+ <Menu size={14} />
500
+ </button>
501
+ </div>
502
+
503
+ {/* Nav */}
504
+ <nav style={{ flex:1, overflowY:'auto', overflowX:'hidden', padding:'12px 8px', display:'flex', flexDirection:'column', gap:4 }}>
505
+ <NavBtn id="home" icon={Home} label="Dashboard" />
506
+ <NavBtn id="history" icon={Clock} label="History" />
507
+
508
+ {sidebarOpen && activeTab === 'history' && (
509
+ <div style={{ marginTop:8, display:'flex', flexDirection:'column', gap:2 }}>
510
+ {history.map(item => (
511
+ <button
512
+ key={item.id}
513
+ onClick={() => { if (item.analysis) { setActiveAnalysis(item.analysis); setActiveTab('home'); } }}
514
+ style={{ display:'flex', alignItems:'flex-start', gap:10, padding:'10px 12px', borderRadius:10, cursor:'pointer', background:'transparent', border:'1px solid transparent', textAlign:'left', transition:'all .2s', width:'100%' }}
515
+ onMouseEnter={e => e.currentTarget.style.background = T.navHoverBg}
516
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
517
+ >
518
+ <FileText size={13} style={{ color:T.t3, flexShrink:0, marginTop:2 }} />
519
+ <div style={{ minWidth:0 }}>
520
+ <p style={{ ...f, fontSize:12, fontWeight:500, color:T.t2, margin:'0 0 3px 0', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:150 }}>{item.filename}</p>
521
+ <p style={{ ...mono, fontSize:10, color:T.t3, margin:0 }}>{item.date}</p>
522
+ </div>
523
+ </button>
524
+ ))}
525
+ </div>
526
+ )}
527
+ </nav>
528
+
529
+ {/* Dark mode toggle */}
530
+ <div style={{ padding:'12px 8px', borderTop:`1px solid ${T.headerBorder}` }}>
531
+ <button
532
+ onClick={() => setDark(v => !v)}
533
+ style={{
534
+ width:'100%', display:'flex', alignItems:'center',
535
+ gap:12, padding: sidebarOpen ? '10px 14px' : '10px',
536
+ justifyContent: sidebarOpen ? 'flex-start' : 'center',
537
+ borderRadius:12, cursor:'pointer',
538
+ background:T.cardBg, border:`1px solid ${T.cardBorder}`,
539
+ color:T.t3, ...f, fontSize:13, fontWeight:500,
540
+ boxShadow:T.cardShadow, transition:'all .2s',
541
+ }}
542
+ onMouseEnter={e => { e.currentTarget.style.borderColor=T.acc; e.currentTarget.style.color=T.acc; e.currentTarget.style.transform='scale(1.02)'; }}
543
+ onMouseLeave={e => { e.currentTarget.style.borderColor=T.cardBorder; e.currentTarget.style.color=T.t3; e.currentTarget.style.transform='none'; }}
544
+ >
545
+ {dark
546
+ ? <Sun size={15} style={{ color:'#22d3ee', flexShrink:0 }} />
547
+ : <Moon size={15} style={{ color:T.acc, flexShrink:0 }} />
548
+ }
549
+ {sidebarOpen && <span>{dark ? 'Light Mode' : 'Dark Mode'}</span>}
550
+ </button>
551
+ </div>
552
+ </aside>
553
+
554
+ {/* ── MAIN AREA ── */}
555
+ <div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden' }}>
556
+
557
+ {/* Top bar */}
558
+ <header style={{ flexShrink:0, display:'flex', alignItems:'center', gap:16, padding:'16px 28px', background:T.headerBg, borderBottom:`1px solid ${T.headerBorder}`, transition:'background .4s, border-color .4s' }}>
559
+ <div style={{ flex:1 }}>
560
+ <h1 style={{ ...syne, fontSize:18, fontWeight:700, color:T.t1, margin:0 }}>
561
+ {activeTab === 'home' ? 'Policy Analysis' : 'Analysis History'}
562
+ </h1>
563
+ <p style={{ ...mono, fontSize:11, color:T.t3, margin:'3px 0 0 0' }}>
564
+ {activeTab === 'home'
565
+ ? activeAnalysis ? `Viewing: ${activeAnalysis.policy_name}` : 'Upload a policy document to begin'
566
+ : `${history.length} document${history.length !== 1 ? 's' : ''} analyzed`}
567
+ </p>
568
+ </div>
569
+
570
+ {/* Header upload button — always visible */}
571
+ <label htmlFor="hdr-upload" style={{
572
+ display:'flex', alignItems:'center', gap:8, padding:'10px 20px', borderRadius:12, cursor:'pointer',
573
+ background:T.accGrad, border:'none', color: dark ? '#0c0908' : '#ffffff',
574
+ ...syne, fontSize:13, fontWeight:600, boxShadow:T.msgUserShadow, transition:'all .2s',
575
+ }}
576
+ onMouseEnter={e => e.currentTarget.style.transform='scale(1.03)'}
577
+ onMouseLeave={e => e.currentTarget.style.transform='none'}
578
+ >
579
+ <FilePlus size={15} /> Upload Policy
580
+ </label>
581
+ <input id="hdr-upload" ref={hdrRef} type="file" accept=".pdf,.doc,.docx"
582
+ style={{ display:'none' }}
583
+ onChange={e => { if (e.target.files[0]) processFile(e.target.files[0]); }}
584
+ />
585
+ </header>
586
+
587
+ {/* Scrollable content */}
588
+ <div className="cb-scrollbar" style={{ flex:1, overflowY:'auto', padding:28, display:'flex', flexDirection:'column', gap:24, background:T.pageBg, transition:'background .4s' }}>
589
+
590
+ {/* FIX 2: Only show full upload zone when no analysis is loaded */}
591
+ {!activeAnalysis && <UploadZone />}
592
+
593
+ {/* Home tab */}
594
+ {activeTab === 'home' && (
595
+ activeAnalysis
596
+ ? renderAnalysis(activeAnalysis)
597
+ : (
598
+ <div className="fade-in" style={{ borderRadius:24, padding:'72px 24px', textAlign:'center', background:T.cardBg, border:`1px solid ${T.cardBorder}`, boxShadow:T.cardShadow, display:'flex', flexDirection:'column', alignItems:'center', gap:20 }}>
599
+ <div style={{ width:80, height:80, borderRadius:24, display:'flex', alignItems:'center', justifyContent:'center', background:T.accGrad, boxShadow:`0 0 40px ${dark ? 'rgba(34,211,238,.2)' : 'rgba(13,148,136,.2)'}` }}>
600
+ <Aperture size={38} color={dark ? '#0c0908' : '#ffffff'} />
601
+ </div>
602
+ <div>
603
+ <h2 style={{ ...bbs, fontSize:48, letterSpacing:2, color:T.t1, margin:'0 0 8px 0', lineHeight:1 }}>READY TO ANALYZE</h2>
604
+ <p style={{ ...f, fontSize:14, color:T.t3, margin:0, maxWidth:420 }}>
605
+ Upload an insurance policy document above. IRIS will extract and structure every clause, benefit, exclusion, and condition for you.
606
+ </p>
607
+ </div>
608
+ <div style={{ display:'flex', gap:10, flexWrap:'wrap', justifyContent:'center', marginTop:8 }}>
609
+ {['Policy Details','Key Benefits','Exclusions','Death Benefit','Tax & Loans','Important Conditions'].map(tag => (
610
+ <span key={tag} style={{ padding:'6px 14px', borderRadius:20, background:T.accBg, border:`1px solid ${T.cardBorder}`, ...mono, fontSize:11, color:T.acc, fontWeight:500 }}>{tag}</span>
611
+ ))}
612
+ </div>
613
+ </div>
614
+ )
615
+ )}
616
+
617
+ {/* History tab */}
618
+ {activeTab === 'history' && renderHistory()}
619
+ </div>
620
+ </div>
621
+
622
+ {/* ── IRIS CHAT FAB ── */}
623
+ {!chatOpen && (
624
+ <button
625
+ onClick={() => setChatOpen(true)}
626
+ style={{
627
+ position:'fixed', bottom:28, right:28, zIndex:100,
628
+ width:58, height:58, borderRadius:18, border:'none', cursor:'pointer',
629
+ display:'flex', alignItems:'center', justifyContent:'center',
630
+ background:T.accGrad,
631
+ boxShadow:`${T.msgUserShadow}, 0 0 30px ${dark ? 'rgba(34,211,238,.3)' : 'rgba(13,148,136,.25)'}`,
632
+ transition:'transform .2s',
633
+ }}
634
+ onMouseEnter={e => e.currentTarget.style.transform='scale(1.08)'}
635
+ onMouseLeave={e => e.currentTarget.style.transform='none'}
636
+ >
637
+ <Aperture size={26} color={dark ? '#0c0908' : '#ffffff'} />
638
+ </button>
639
+ )}
640
+
641
+ {/* ── IRIS CHAT PANEL ── */}
642
+ {chatOpen && (
643
+ <div className="fade-in" style={{
644
+ position:'fixed', bottom:28, right:28, zIndex:100,
645
+ width:360, height:500, display:'flex', flexDirection:'column',
646
+ borderRadius:24, overflow:'hidden',
647
+ background:T.cardBg, border:`1px solid ${T.cardBorder}`,
648
+ boxShadow:`${T.cardShadow}, 0 0 40px ${dark ? 'rgba(34,211,238,.15)' : 'rgba(13,148,136,.15)'}`,
649
+ }}>
650
+
651
+ {/* Chat header */}
652
+ <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'16px 20px', background:T.headerBg, borderBottom:`1px solid ${T.headerBorder}` }}>
653
+ <div style={{ display:'flex', alignItems:'center', gap:12 }}>
654
+ <IrisAvatar size={40} />
655
+ <div>
656
+ <p style={{ ...syne, margin:0, fontSize:15, fontWeight:600, color:T.t1 }}>IRIS</p>
657
+ <div style={{ display:'flex', alignItems:'center', gap:6, marginTop:3 }}>
658
+ <span style={{ width:7, height:7, borderRadius:'50%', display:'inline-block', background:T.statusDot, boxShadow:T.statusGlow, animation:'cb-pulse 2s ease-in-out infinite' }} />
659
+ <span style={{ ...mono, fontSize:10, color:T.onlineText, fontWeight:500 }}>System Online</span>
660
+ </div>
661
+ </div>
662
+ </div>
663
+ <button
664
+ onClick={() => setChatOpen(false)}
665
+ style={{ width:30, height:30, borderRadius:8, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', background:'transparent', border:`1px solid ${T.cardBorder}`, color:T.t3, transition:'all .2s' }}
666
+ onMouseEnter={e => { e.currentTarget.style.borderColor=T.redClr; e.currentTarget.style.color=T.redClr; }}
667
+ onMouseLeave={e => { e.currentTarget.style.borderColor=T.cardBorder; e.currentTarget.style.color=T.t3; }}
668
+ >
669
+ <X size={14} />
670
+ </button>
671
+ </div>
672
+
673
+ {/* Messages */}
674
+ <div className="cb-scrollbar" style={{ flex:1, overflowY:'auto', padding:'20px 16px', display:'flex', flexDirection:'column', gap:16, background:T.msgAreaBg }}>
675
+ {chatMessages.map(msg => {
676
+ const isUser = msg.sender === 'user';
677
+ return (
678
+ <div key={msg.id} className="cb-msg" style={{ display:'flex', gap:10, width:'100%', justifyContent: isUser ? 'flex-end' : 'flex-start', alignItems:'flex-end' }}>
679
+ {!isUser && (
680
+ <div style={{ width:32, height:32, borderRadius:10, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.msgAiBg, border:`1px solid ${T.cardBorder}` }}>
681
+ <Aperture size={15} style={{ color:T.acc }} />
682
+ </div>
683
+ )}
684
+ <div style={{
685
+ maxWidth:'80%', padding:'12px 16px', fontSize:13, lineHeight:1.6,
686
+ borderRadius: isUser ? '18px 4px 18px 18px' : '4px 18px 18px 18px',
687
+ background: isUser ? T.msgUserBg : T.msgAiBg,
688
+ color: isUser ? T.msgUserText : T.msgAiText,
689
+ boxShadow: isUser ? T.msgUserShadow : 'none',
690
+ ...f, transition:'background .4s, color .4s',
691
+ }}>
692
+ {msg.text}
693
+ </div>
694
+ </div>
695
+ );
696
+ })}
697
+ {isTyping && (
698
+ <div className="cb-msg" style={{ display:'flex', gap:10, alignItems:'center' }}>
699
+ <div style={{ width:32, height:32, borderRadius:10, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.msgAiBg, border:`1px solid ${T.cardBorder}` }}>
700
+ <Loader2 size={14} style={{ color:T.acc, animation:'spin 1s linear infinite' }} />
701
+ </div>
702
+ <div style={{ padding:'14px 18px', display:'flex', gap:5, alignItems:'center', borderRadius:'4px 18px 18px 18px', background:T.msgAiBg }}>
703
+ <span className="cb-db1" style={{ width:6, height:6, borderRadius:'50%', background:T.acc, display:'inline-block' }} />
704
+ <span className="cb-db2" style={{ width:6, height:6, borderRadius:'50%', background:T.acc, display:'inline-block' }} />
705
+ <span className="cb-db3" style={{ width:6, height:6, borderRadius:'50%', background:T.acc, display:'inline-block' }} />
706
+ </div>
707
+ </div>
708
+ )}
709
+ <div ref={chatEnd} />
710
+ </div>
711
+
712
+ {/* Input */}
713
+ <div style={{ padding:'14px 16px 18px', background:T.headerBg, borderTop:`1px solid ${T.headerBorder}` }}>
714
+ <div style={{ display:'flex', alignItems:'center', gap:10, padding:'10px 14px', borderRadius:14, background:T.inputBg, border:`1px solid ${T.inputBorder}`, transition:'border-color .2s' }}>
715
+ <input
716
+ type="text" value={chatInput}
717
+ onChange={e => setChatInput(e.target.value)}
718
+ onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) sendChat(); }}
719
+ placeholder="Ask IRIS about this policy…"
720
+ style={{ flex:1, background:'transparent', border:'none', outline:'none', fontSize:13, color:T.inputText, ...f }}
721
+ onFocus={e => e.target.parentNode.style.borderColor = T.inputFocus}
722
+ onBlur={e => e.target.parentNode.style.borderColor = T.inputBorder}
723
+ />
724
+ <button
725
+ onClick={sendChat}
726
+ disabled={!chatInput.trim() || isTyping}
727
+ style={{
728
+ flexShrink:0, width:34, height:34, borderRadius:10, border:'none', cursor:'pointer',
729
+ display:'flex', alignItems:'center', justifyContent:'center',
730
+ background: chatInput.trim() && !isTyping ? T.sendActive : T.sendDisabled,
731
+ color: dark ? '#0c0908' : '#ffffff',
732
+ opacity: chatInput.trim() && !isTyping ? 1 : 0.4,
733
+ boxShadow: chatInput.trim() && !isTyping ? T.msgUserShadow : 'none',
734
+ transition:'all .2s',
735
+ }}
736
+ onMouseEnter={e => { if (chatInput.trim() && !isTyping) e.currentTarget.style.transform='scale(1.08)'; }}
737
+ onMouseLeave={e => e.currentTarget.style.transform='none'}
738
+ >
739
+ <Send size={15} />
740
+ </button>
741
+ </div>
742
+ <p style={{ ...mono, marginTop:10, textAlign:'center', fontSize:10, letterSpacing:1, color:T.t3 }}>
743
+ IRIS insights are for guidance · Not legal advice
744
+ </p>
745
+ </div>
746
+ </div>
747
+ )}
748
+
749
+ <style>{`.cb-scrollbar::-webkit-scrollbar-thumb { background: ${T.scrollThumb}; }`}</style>
750
+ </div>
751
+ );
752
+ }
frontend/src/components/Dashboard/UploadModal.jsx ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+ import {
3
+ UploadCloud, FileText, X, ShieldCheck, HelpCircle,
4
+ CheckCircle2, Loader, Cpu, Zap, Lock, Eye, Sun, Moon, AlertTriangle
5
+ } from 'lucide-react';
6
+
7
+ /* ── fonts + keyframes ──────────────────────────────────── */
8
+ const FONT_LINK =
9
+ 'https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Syne:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600&display=swap';
10
+
11
+ const KEYFRAMES = `
12
+ @keyframes um-up { from{opacity:0;transform:translateY(20px) scale(.97)} to{opacity:1;transform:none} }
13
+ @keyframes um-scan { 0%{top:-2px;opacity:0} 4%{opacity:1} 96%{opacity:1} 100%{top:100%;opacity:0} }
14
+ @keyframes um-spin { to{transform:rotate(360deg)} }
15
+ @keyframes um-float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-7px)} }
16
+ @keyframes um-fadeR { from{opacity:0;transform:translateX(10px)} to{opacity:1;transform:none} }
17
+ @keyframes um-shim { 0%{transform:translateX(-120%)} 100%{transform:translateX(320%)} }
18
+ @keyframes um-twinkle{ 0%,100%{opacity:.1;transform:scale(.7)} 50%{opacity:.9;transform:scale(1.3)} }
19
+ @keyframes um-orbit { from{transform:rotate(0deg) translateX(180px) rotate(0deg)} to{transform:rotate(360deg) translateX(180px) rotate(-360deg)} }
20
+ @keyframes um-orbit2{ from{transform:rotate(120deg) translateX(240px) rotate(-120deg)} to{transform:rotate(480deg) translateX(240px) rotate(-480deg)} }
21
+ @keyframes um-pulse { 0%,100%{opacity:.12;transform:scale(1)} 50%{opacity:.26;transform:scale(1.07)} }
22
+ @keyframes um-sway { 0%,100%{transform:rotate(-4deg) scale(1)} 50%{transform:rotate(4deg) scale(1.03)} }
23
+ @keyframes um-shake { 0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-4px)} 40%,80%{transform:translateX(4px)} }
24
+
25
+ .um-card { animation: um-up .44s cubic-bezier(.16,1,.3,1) forwards }
26
+ .um-scan { animation: um-scan 2.6s linear infinite }
27
+ .um-spin { animation: um-spin 1s linear infinite }
28
+ .um-float { animation: um-float 4s ease-in-out infinite }
29
+ .um-fadeR { animation: um-fadeR .3s ease forwards }
30
+ .um-shake { animation: um-shake .4s ease forwards }
31
+ .um-body::-webkit-scrollbar{display:none}
32
+ .um-shim::after {
33
+ content:'';position:absolute;inset:0;
34
+ background:linear-gradient(90deg,transparent,rgba(255,255,255,.28),transparent);
35
+ animation: um-shim 1.8s ease infinite;
36
+ }
37
+ `;
38
+
39
+ /* ── waveform bar heights (deterministic) ─────────────────── */
40
+ const BARS = Array.from({ length: 44 }, (_, i) =>
41
+ Math.max(5, Math.min(52,
42
+ 10 + Math.abs(Math.sin(i * 0.38 + 1.2) * 28) + Math.abs(Math.cos(i * 0.7) * 14)
43
+ ))
44
+ );
45
+
46
+ /* ── star positions (deterministic) ──────────────────────── */
47
+ const STARS = Array.from({ length: 88 }, (_, i) => ({
48
+ x: ((i * 137.508) % 100).toFixed(2),
49
+ y: ((i * 97.3) % 100).toFixed(2),
50
+ r: (0.5 + (i % 4) * 0.45).toFixed(1),
51
+ delay: ((i * 0.22) % 3).toFixed(2),
52
+ dur: (2 + (i % 5) * 0.7).toFixed(1),
53
+ }));
54
+
55
+ /* ── THEME OBJECTS — matched to LoginPage / Hero palette ─── */
56
+ const LIGHT = {
57
+ pageBg: '#ffffff',
58
+ cardBg: '#ffffff',
59
+ leftPanelBg: '#f9fafb',
60
+ leftBorder: '#e5e7eb',
61
+ headerBorder: '#e5e7eb',
62
+ footerBg: '#f9fafb',
63
+ footerBorder: '#e5e7eb',
64
+ dropZoneBg: '#f9fafb',
65
+ dropZoneBorder: '#d1fae5',
66
+ dropZoneDrag: '#ccfbf1',
67
+ dropZoneDragBorder: '#0d9488',
68
+ chipBg: '#ffffff',
69
+ chipBorder: '#e5e7eb',
70
+ stageBg: '#f9fafb',
71
+ stageBorder: '#e5e7eb',
72
+ stageActiveBg: '#f0fdfa',
73
+ stageActiveBorder: '#99f6e4',
74
+ cardShadow: '0 0 0 1px rgba(0,0,0,.05), 0 32px 80px rgba(0,0,0,.08)',
75
+ topStrip: 'linear-gradient(90deg,#0d9488,#14b8a6,#0d9488)',
76
+ t1: '#111827',
77
+ t2: '#6b7280',
78
+ t3: '#9ca3af',
79
+ t4: 'rgba(209,213,219,.8)',
80
+ acc: '#0d9488',
81
+ acc2: '#14b8a6',
82
+ accGrad: 'linear-gradient(135deg,#0d9488,#14b8a6)',
83
+ brandGrad: 'linear-gradient(135deg,#0d9488,#14b8a6)',
84
+ scanBeam: 'linear-gradient(90deg,transparent,#0d9488,#5eead4,transparent)',
85
+ scanGlow: '0 0 12px #0d9488',
86
+ progressBg: '#ccfbf1',
87
+ progressFill: 'linear-gradient(90deg,#0d9488,#14b8a6)',
88
+ pillBg: 'rgba(204,251,241,.5)',
89
+ pillBorder: 'rgba(153,246,228,.8)',
90
+ pillText: '#0f766e',
91
+ pdfBg: '#fee2e2',
92
+ pdfBorder: '#fecaca',
93
+ pdfText: '#dc2626',
94
+ toggleBg: '#f3f4f6',
95
+ toggleBorder: '#e5e7eb',
96
+ toggleColor: '#0d9488',
97
+ clearBg: '#f9fafb',
98
+ clearBorder: '#e5e7eb',
99
+ clearText: '#6b7280',
100
+ clearHover: '#f3f4f6',
101
+ analyseGrad: 'linear-gradient(135deg,#0d9488,#14b8a6)',
102
+ analyseShadow: '0 6px 20px rgba(13,148,136,.3)',
103
+ analyseText: '#ffffff',
104
+ disabledBg: '#f3f4f6',
105
+ disabledText: '#9ca3af',
106
+ iconBg: '#f0fdfa',
107
+ iconBorder: '#99f6e4',
108
+ iconColor: '#0d9488',
109
+ waveInactive: '#e5e7eb',
110
+ waveGrad: 'linear-gradient(to top,#0d9488,#5eead4)',
111
+ fileText: '#111827',
112
+ fileSub: '#9ca3af',
113
+ doneBg: '#f0fdfa',
114
+ doneBorder: '#99f6e4',
115
+ doneText: '#0d9488',
116
+ errorText: '#dc2626',
117
+ errorBg: '#fee2e2',
118
+ };
119
+
120
+ const DARK = {
121
+ pageBg: '#0c0908',
122
+ cardBg: '#15100d',
123
+ leftPanelBg: '#080605',
124
+ leftBorder: 'rgba(42,31,26,.9)',
125
+ headerBorder: 'rgba(42,31,26,.9)',
126
+ footerBg: 'rgba(8,6,5,.9)',
127
+ footerBorder: 'rgba(42,31,26,.9)',
128
+ dropZoneBg: 'rgba(34,211,238,.03)',
129
+ dropZoneBorder: 'rgba(34,211,238,.2)',
130
+ dropZoneDrag: 'rgba(34,211,238,.08)',
131
+ dropZoneDragBorder: '#22d3ee',
132
+ chipBg: 'rgba(34,211,238,.06)',
133
+ chipBorder: 'rgba(42,31,26,.9)',
134
+ stageBg: 'rgba(34,211,238,.03)',
135
+ stageBorder: 'rgba(42,31,26,.9)',
136
+ stageActiveBg: 'rgba(34,211,238,.08)',
137
+ stageActiveBorder: 'rgba(34,211,238,.25)',
138
+ cardShadow: '0 0 0 1px rgba(42,31,26,.9), 0 40px 120px rgba(0,0,0,.7)',
139
+ topStrip: 'linear-gradient(90deg,#0e7490,#22d3ee,#0e7490)',
140
+ t1: '#ecfeff',
141
+ t2: 'rgba(207,250,254,.6)',
142
+ t3: 'rgba(207,250,254,.35)',
143
+ t4: 'rgba(34,211,238,.2)',
144
+ acc: '#22d3ee',
145
+ acc2: '#67e8f9',
146
+ accGrad: 'linear-gradient(135deg,#0e7490,#22d3ee)',
147
+ brandGrad: 'linear-gradient(135deg,#0e7490,#22d3ee)',
148
+ scanBeam: 'linear-gradient(90deg,transparent,#22d3ee,#67e8f9,transparent)',
149
+ scanGlow: '0 0 14px #22d3ee',
150
+ progressBg: 'rgba(34,211,238,.1)',
151
+ progressFill: 'linear-gradient(90deg,#22d3ee,#0ea5e9)',
152
+ pillBg: 'rgba(21,94,117,.18)',
153
+ pillBorder: 'rgba(21,94,117,.4)',
154
+ pillText: '#a5f3fc',
155
+ pdfBg: 'rgba(239,68,68,.1)',
156
+ pdfBorder: 'rgba(239,68,68,.25)',
157
+ pdfText: '#fca5a5',
158
+ toggleBg: '#1a1310',
159
+ toggleBorder: '#2a1f1a',
160
+ toggleColor: '#22d3ee',
161
+ clearBg: 'rgba(34,211,238,.07)',
162
+ clearBorder: 'rgba(42,31,26,.9)',
163
+ clearText: 'rgba(207,250,254,.6)',
164
+ clearHover: 'rgba(34,211,238,.14)',
165
+ analyseGrad: '#e0f2fe',
166
+ analyseShadow: '0 6px 24px rgba(34,211,238,.2)',
167
+ analyseText: '#0c0908',
168
+ disabledBg: 'rgba(34,211,238,.06)',
169
+ disabledText: 'rgba(34,211,238,.25)',
170
+ iconBg: 'rgba(34,211,238,.08)',
171
+ iconBorder: 'rgba(42,31,26,.9)',
172
+ iconColor: '#22d3ee',
173
+ waveInactive: 'rgba(34,211,238,.12)',
174
+ waveGrad: 'linear-gradient(to top,#22d3ee,#a5f3fc)',
175
+ fileText: '#ecfeff',
176
+ fileSub: 'rgba(34,211,238,.45)',
177
+ doneBg: 'rgba(34,211,238,.1)',
178
+ doneBorder: 'rgba(34,211,238,.25)',
179
+ doneText: '#22d3ee',
180
+ errorText: '#fca5a5',
181
+ errorBg: 'rgba(239,68,68,.15)',
182
+ };
183
+
184
+ /* ── Corner Bracket ─────────────────────────── */
185
+ function Bracket({ corner, active, T }) {
186
+ const t = corner.includes('top'), l = corner.includes('left');
187
+ const s = active ? 28 : 14;
188
+ const col = active ? T.acc : T.t4;
189
+ return (
190
+ <div style={{
191
+ position: 'absolute', width: s, height: s,
192
+ transition: 'all .35s cubic-bezier(.16,1,.3,1)',
193
+ [t ? 'top' : 'bottom']: 10, [l ? 'left' : 'right']: 10,
194
+ borderTop: t ? `2px solid ${col}` : 'none',
195
+ borderBottom: !t ? `2px solid ${col}` : 'none',
196
+ borderLeft: l ? `2px solid ${col}` : 'none',
197
+ borderRight: !l ? `2px solid ${col}` : 'none',
198
+ }} />
199
+ );
200
+ }
201
+
202
+ const STAGES = [
203
+ { icon: Cpu, label: 'Parse' },
204
+ { icon: Eye, label: 'Extract' },
205
+ { icon: Zap, label: 'Index' },
206
+ { icon: Lock, label: 'Secure' },
207
+ ];
208
+
209
+ /* ══════════════════════ MAIN COMPONENT ═══════════════════════ */
210
+ export default function UploadModal({ onUploadComplete, onCancel }) {
211
+ const [drag, setDrag] = useState(false);
212
+ const [file, setFile] = useState(null);
213
+ const [status, setStatus] = useState('idle');
214
+ const [error, setError] = useState('');
215
+ const [pct, setPct] = useState(0);
216
+ const [stage, setStage] = useState(-1);
217
+ const [dark, setDark] = useState(false);
218
+
219
+ const fileRef = useRef(null);
220
+ const dragCounter = useRef(0);
221
+ const T = dark ? DARK : LIGHT;
222
+
223
+ useEffect(() => {
224
+ const link = document.createElement('link');
225
+ link.rel = 'stylesheet'; link.href = FONT_LINK;
226
+ const style = document.createElement('style');
227
+ style.textContent = KEYFRAMES;
228
+ document.head.append(link, style);
229
+ return () => { link.remove(); style.remove(); };
230
+ }, []);
231
+
232
+ const stageRef = useRef(-1);
233
+ useEffect(() => {
234
+ if (status !== 'uploading') return;
235
+ setPct(0); setStage(0); stageRef.current = 0;
236
+ const id = setInterval(() => {
237
+ setPct(p => {
238
+ const n = p + (p < 50 ? 2.8 : p < 78 ? 1.5 : 0.5);
239
+ if (n >= 25 && stageRef.current < 1) { stageRef.current = 1; setStage(1); }
240
+ if (n >= 55 && stageRef.current < 2) { stageRef.current = 2; setStage(2); }
241
+ if (n >= 80 && stageRef.current < 3) { stageRef.current = 3; setStage(3); }
242
+ if (n >= 100) { clearInterval(id); return 100; }
243
+ return n;
244
+ });
245
+ }, 60);
246
+ return () => clearInterval(id);
247
+ }, [status]);
248
+
249
+ useEffect(() => {
250
+ if (pct >= 100 && status === 'uploading')
251
+ setTimeout(() => { setStatus('done'); setTimeout(() => onUploadComplete(file), 800); }, 250);
252
+ }, [pct, status, file, onUploadComplete]);
253
+
254
+ const validateAndPick = useCallback((f) => {
255
+ setError('');
256
+ if (f.type !== 'application/pdf') {
257
+ setError('Invalid file type. Please upload a PDF document.');
258
+ return;
259
+ }
260
+ if (f.size > 50 * 1024 * 1024) {
261
+ setError('File is too large. Maximum size is 50 MB.');
262
+ return;
263
+ }
264
+ setFile(f); setStatus('idle'); setPct(0); setStage(-1);
265
+ }, []);
266
+
267
+ const handleDragEnter = (e) => {
268
+ e.preventDefault(); e.stopPropagation();
269
+ dragCounter.current += 1;
270
+ if (e.dataTransfer.items && e.dataTransfer.items.length > 0) setDrag(true);
271
+ };
272
+ const handleDragLeave = (e) => {
273
+ e.preventDefault(); e.stopPropagation();
274
+ dragCounter.current -= 1;
275
+ if (dragCounter.current === 0) setDrag(false);
276
+ };
277
+ const handleDrop = (e) => {
278
+ e.preventDefault(); e.stopPropagation();
279
+ setDrag(false); dragCounter.current = 0;
280
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
281
+ validateAndPick(e.dataTransfer.files[0]);
282
+ }
283
+ };
284
+
285
+ const reset = () => { setFile(null); setStatus('idle'); setPct(0); setStage(-1); setError(''); };
286
+ const uploading = status === 'uploading';
287
+ const done = status === 'done';
288
+ const hasFile = !!file;
289
+
290
+ /* ── Font style objects — matched to LoginPage ── */
291
+ const f = { fontFamily: "'DM Sans', sans-serif" };
292
+ const syne = { fontFamily: "'Syne', sans-serif" };
293
+ const bebas = { fontFamily: "'Bebas Neue', cursive" };
294
+ const mono = { fontFamily: "'JetBrains Mono', monospace" };
295
+
296
+ const getProgressText = () => {
297
+ if (pct < 25) return "Parsing contract structure...";
298
+ if (pct < 55) return "Extracting key entities & dates...";
299
+ if (pct < 85) return "Running risk analysis models...";
300
+ return "Finalizing encryption...";
301
+ };
302
+
303
+ return (
304
+ <div style={f}>
305
+ {/* ══ FULL-SCREEN OVERLAY ══════════════════════════════ */}
306
+ <div style={{
307
+ position:'fixed', inset:0, zIndex:50,
308
+ display:'flex', alignItems:'center', justifyContent:'center',
309
+ overflow:'hidden', background: T.pageBg, transition:'background .5s ease',
310
+ }}>
311
+
312
+ {/* ── DARK BG: Hero-style starfield + black-brown atmosphere ── */}
313
+ {dark && (
314
+ <div style={{ position:'absolute', inset:0, pointerEvents:'none' }}>
315
+ <svg style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}>
316
+ {STARS.map((s, i) => (
317
+ <circle key={i} cx={`${s.x}%`} cy={`${s.y}%`} r={s.r} fill="#a5f3fc"
318
+ style={{ animation:`um-twinkle ${s.dur}s ease-in-out infinite`,
319
+ animationDelay:`${s.delay}s`, opacity:.12 }}/>
320
+ ))}
321
+ </svg>
322
+ {/* top cyan glow — matches Hero dark atmosphere */}
323
+ <div style={{ position:'absolute', width:700, height:700, borderRadius:'50%',
324
+ top:'30%', left:'50%', transform:'translate(-50%,-50%)', filter:'blur(80px)',
325
+ background:'radial-gradient(circle,rgba(34,211,238,.1) 0%,transparent 65%)',
326
+ animation:'um-pulse 6s ease-in-out infinite' }}/>
327
+ <div style={{ position:'absolute', width:500, height:500, borderRadius:'50%',
328
+ top:'-15%', left:'-10%', filter:'blur(90px)',
329
+ background:'radial-gradient(circle,rgba(14,116,144,.12) 0%,transparent 65%)',
330
+ animation:'um-pulse 8s ease-in-out infinite', animationDelay:'.8s' }}/>
331
+ <div style={{ position:'absolute', width:420, height:420, borderRadius:'50%',
332
+ bottom:'-12%', right:'-8%', filter:'blur(80px)',
333
+ background:'radial-gradient(circle,rgba(34,211,238,.07) 0%,transparent 65%)',
334
+ animation:'um-pulse 7s ease-in-out infinite', animationDelay:'2s' }}/>
335
+ {/* orbiting dots */}
336
+ <div style={{ position:'absolute', top:'50%', left:'50%', width:0, height:0 }}>
337
+ <div style={{ position:'absolute', width:5, height:5, borderRadius:'50%',
338
+ background:'#22d3ee', boxShadow:'0 0 8px #22d3ee', marginLeft:-2.5, marginTop:-2.5,
339
+ animation:'um-orbit 14s linear infinite', opacity:.6 }}/>
340
+ <div style={{ position:'absolute', width:3, height:3, borderRadius:'50%',
341
+ background:'#a5f3fc', boxShadow:'0 0 5px #a5f3fc', marginLeft:-1.5, marginTop:-1.5,
342
+ animation:'um-orbit2 20s linear infinite', opacity:.4 }}/>
343
+ </div>
344
+ {/* subtle grid */}
345
+ <svg style={{ position:'absolute', inset:0, width:'100%', height:'100%', opacity:.03 }}>
346
+ <defs><pattern id="cg" x="0" y="0" width="56" height="56" patternUnits="userSpaceOnUse">
347
+ <path d="M56 0L0 0 0 56" fill="none" stroke="#22d3ee" strokeWidth=".6"/>
348
+ </pattern></defs>
349
+ <rect width="100%" height="100%" fill="url(#cg)"/>
350
+ </svg>
351
+ </div>
352
+ )}
353
+
354
+ {/* ── LIGHT BG: teal botanical (matches LoginPage light panel) ── */}
355
+ {!dark && (
356
+ <div style={{ position:'absolute', inset:0, pointerEvents:'none', overflow:'hidden' }}>
357
+ <svg style={{ position:'absolute', top:'-5%', right:'-8%', width:380, height:380,
358
+ animation:'um-sway 8s ease-in-out infinite', opacity:.1 }}>
359
+ <ellipse cx="190" cy="190" rx="160" ry="72" fill="#14b8a6" transform="rotate(-28 190 190)"/>
360
+ <ellipse cx="190" cy="190" rx="140" ry="56" fill="#0d9488" transform="rotate(14 190 190)"/>
361
+ <ellipse cx="190" cy="190" rx="90" ry="32" fill="#5eead4" transform="rotate(-5 190 190)"/>
362
+ </svg>
363
+ <svg style={{ position:'absolute', bottom:'-8%', left:'-5%', width:320, height:320,
364
+ animation:'um-sway 10s ease-in-out infinite', animationDelay:'1.5s', opacity:.08 }}>
365
+ <ellipse cx="160" cy="160" rx="130" ry="60" fill="#0d9488" transform="rotate(22 160 160)"/>
366
+ <ellipse cx="160" cy="160" rx="80" ry="35" fill="#14b8a6" transform="rotate(-10 160 160)"/>
367
+ </svg>
368
+ <svg style={{ position:'absolute', top:'30%', left:'-3%', width:200, height:200,
369
+ animation:'um-sway 12s ease-in-out infinite', animationDelay:'3s', opacity:.06 }}>
370
+ <ellipse cx="100" cy="100" rx="80" ry="38" fill="#14b8a6" transform="rotate(35 100 100)"/>
371
+ </svg>
372
+ {/* teal glow */}
373
+ <div style={{ position:'absolute', width:600, height:600, borderRadius:'50%',
374
+ top:'-20%', left:'-10%', filter:'blur(80px)',
375
+ background:'radial-gradient(circle,rgba(94,234,212,.3) 0%,transparent 65%)',
376
+ animation:'um-pulse 7s ease-in-out infinite' }}/>
377
+ <div style={{ position:'absolute', width:500, height:500, borderRadius:'50%',
378
+ bottom:'-18%', right:'-8%', filter:'blur(80px)',
379
+ background:'radial-gradient(circle,rgba(20,184,166,.25) 0%,transparent 65%)',
380
+ animation:'um-pulse 9s ease-in-out infinite', animationDelay:'1.2s' }}/>
381
+ {/* dot grid */}
382
+ <svg style={{ position:'absolute', inset:0, width:'100%', height:'100%', opacity:.05 }}>
383
+ <defs><pattern id="gg" x="0" y="0" width="22" height="22" patternUnits="userSpaceOnUse">
384
+ <circle cx="1" cy="1" r="1" fill="#0d9488"/>
385
+ </pattern></defs>
386
+ <rect width="100%" height="100%" fill="url(#gg)"/>
387
+ </svg>
388
+ </div>
389
+ )}
390
+
391
+ {/* ══ CARD ═══════════════════════════════════════════ */}
392
+ <div className="um-card" style={{
393
+ position:'relative', zIndex:10, display:'flex', overflow:'hidden',
394
+ width:'min(880px, calc(100vw - 20px))', maxHeight:'calc(100vh - 40px)', borderRadius:26,
395
+ background: T.cardBg, border:`1px solid ${T.leftBorder}`,
396
+ boxShadow: T.cardShadow, transition:'background .4s, border-color .4s, box-shadow .4s',
397
+ }}>
398
+
399
+ {/* ── LEFT PANEL ─────────────────────────────────── */}
400
+ <div style={{
401
+ width:272, flexShrink:0, display:'flex', flexDirection:'column', justifyContent:'space-between',
402
+ padding:32, position:'relative', overflow:'hidden',
403
+ background: T.leftPanelBg, borderRight:`1px solid ${T.leftBorder}`, transition:'background .4s, border-color .4s',
404
+ }}>
405
+ <svg style={{ position:'absolute', inset:0, width:'100%', height:'100%', pointerEvents:'none', opacity:.06 }}>
406
+ <defs><pattern id="lp" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
407
+ <circle cx="1" cy="1" r=".8" fill={T.acc}/>
408
+ </pattern></defs>
409
+ <rect width="100%" height="100%" fill="url(#lp)"/>
410
+ </svg>
411
+
412
+ <div style={{ position:'relative', zIndex:1 }}>
413
+ <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:36 }}>
414
+ <div style={{ width:36, height:36, borderRadius:10, background:T.brandGrad, display:'flex', alignItems:'center', justifyContent:'center', boxShadow:`0 4px 14px ${T.acc}44` }}>
415
+ <Cpu size={16} color="#fff"/>
416
+ </div>
417
+ <span style={{ ...syne, fontWeight: 700, fontSize:19, letterSpacing: 0.5, color:T.t1 }}>
418
+ PolicyLens
419
+ </span>
420
+ </div>
421
+
422
+ {[
423
+ { icon: ShieldCheck, label:'End-to-end encrypted' },
424
+ { icon: Zap, label:'AI clause extraction' },
425
+ { icon: Lock, label:'Zero data retention' },
426
+ { icon: Eye, label:'Risk score in seconds'},
427
+ ].map(({ icon: Icon, label }) => (
428
+ <div key={label} style={{ display:'flex', alignItems:'center', gap:12, marginBottom:18 }}>
429
+ <div style={{ width:30, height:30, borderRadius:8, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background:T.iconBg, border:`1px solid ${T.iconBorder}`, transition:'background .4s' }}>
430
+ <Icon size={13} style={{ color:T.iconColor }}/>
431
+ </div>
432
+ <span style={{ ...f, fontSize:12, color:T.t2 }}>{label}</span>
433
+ </div>
434
+ ))}
435
+ </div>
436
+
437
+ <div style={{ position:'relative', zIndex:1 }}>
438
+ <p style={{ ...mono, fontSize:9, letterSpacing:3, color:T.t3, marginBottom:10 }}>DOC SIGNATURE</p>
439
+ <div style={{ display:'flex', alignItems:'flex-end', gap:2, height:56 }}>
440
+ {BARS.map((h, i) => (
441
+ <div key={i} style={{ width:3, borderRadius:2, height:`${hasFile ? h : 5}px`, background: hasFile ? T.waveGrad : T.waveInactive, transition:`height ${0.28 + i * 0.008}s cubic-bezier(.16,1,.3,1)` }}/>
442
+ ))}
443
+ </div>
444
+ {hasFile && (
445
+ <p className="um-fadeR" style={{ ...mono, marginTop:8, fontSize:10, color:T.acc, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>
446
+ {file.name.slice(0,24)}{file.name.length > 24 ? '…' : ''}
447
+ </p>
448
+ )}
449
+ </div>
450
+ </div>
451
+
452
+ {/* ── RIGHT PANEL ────────────────────────────────── */}
453
+ <div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden' }}>
454
+
455
+ <div style={{ height:3, background:T.topStrip, transition:'background .5s' }}/>
456
+
457
+ <div style={{ padding:'28px 32px 20px', display:'flex', justifyContent:'space-between', alignItems:'flex-start', borderBottom:`1px solid ${T.headerBorder}`, transition:'border-color .4s' }}>
458
+ <div>
459
+ <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:12 }}>
460
+ <span style={{ ...syne, fontWeight: 700, fontSize:10, letterSpacing:'1px', textTransform:'uppercase', padding:'4px 12px', borderRadius:99, background:T.pillBg, border:`1px solid ${T.pillBorder}`, color:T.pillText, transition:'all .4s' }}>
461
+ PolicyLens AI
462
+ </span>
463
+ <span style={{ ...mono, fontSize:10, letterSpacing:'1px', padding:'4px 10px', borderRadius:99, background:T.pdfBg, border:`1px solid ${T.pdfBorder}`, color:T.pdfText, transition:'all .4s' }}>
464
+ .PDF
465
+ </span>
466
+ </div>
467
+ <h2 style={{ ...bebas, fontSize:44, letterSpacing:2, color:T.t1, lineHeight:1, margin:0, transition:'color .4s' }}>
468
+ Upload Document
469
+ </h2>
470
+ <p style={{ ...f, marginTop:8, fontSize:14, color:T.t2, transition:'color .4s' }}>
471
+ Drop a contract or policy — AI extracts every clause in seconds.
472
+ </p>
473
+ </div>
474
+
475
+ <div style={{ display:'flex', alignItems:'center', gap:8 }}>
476
+ <button onClick={() => setDark(v => !v)} style={{ width:36, height:36, borderRadius:10, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', background:T.toggleBg, border:`1px solid ${T.toggleBorder}`, transition:'all .2s' }}
477
+ onMouseEnter={e => e.currentTarget.style.transform='rotate(14deg) scale(1.1)'}
478
+ onMouseLeave={e => e.currentTarget.style.transform='none'}>
479
+ {dark ? <Sun size={14} style={{ color:'#22d3ee' }}/> : <Moon size={14} style={{ color:T.acc }}/>}
480
+ </button>
481
+ <button onClick={onCancel} style={{ width:36, height:36, borderRadius:10, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', background:T.clearBg, border:`1px solid ${T.clearBorder}`, color:T.t2, transition:'all .15s' }}
482
+ onMouseEnter={e => { e.currentTarget.style.background='rgba(239,68,68,.12)'; e.currentTarget.style.borderColor='rgba(239,68,68,.4)'; e.currentTarget.style.color='#f87171'; }}
483
+ onMouseLeave={e => { e.currentTarget.style.background=T.clearBg; e.currentTarget.style.borderColor=T.clearBorder; e.currentTarget.style.color=T.t2; }}>
484
+ <X size={15}/>
485
+ </button>
486
+ </div>
487
+ </div>
488
+
489
+ <div className="um-body" style={{ flex:1, overflowY:'auto', scrollbarWidth:'none', msOverflowStyle:'none', padding:'24px 32px', display:'flex', flexDirection:'column', gap:14 }}>
490
+
491
+ {/* ── DROP ZONE ── */}
492
+ <div
493
+ onDragEnter={handleDragEnter}
494
+ onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
495
+ onDragLeave={handleDragLeave}
496
+ onDrop={handleDrop}
497
+ onClick={() => !file && fileRef.current?.click()}
498
+ style={{
499
+ position:'relative', height:210, borderRadius:20, overflow:'hidden',
500
+ cursor: file ? 'default' : 'pointer', transition:'all .3s',
501
+ background: drag ? T.dropZoneDrag : error ? T.errorBg : T.dropZoneBg,
502
+ border: drag ? `2px solid ${T.dropZoneDragBorder}` : error ? `2px solid ${T.errorText}` : file ? `1px solid ${T.dropZoneBorder}` : `2px dashed ${T.t4}`,
503
+ }}
504
+ >
505
+ <svg style={{ position:'absolute', inset:0, width:'100%', height:'100%', pointerEvents:'none', opacity: drag || hasFile ? .2 : .07, transition:'opacity .4s' }}>
506
+ <defs><pattern id="dz" x="0" y="0" width="18" height="18" patternUnits="userSpaceOnUse">
507
+ <circle cx="1" cy="1" r="1" fill={error ? T.errorText : T.acc}/>
508
+ </pattern></defs>
509
+ <rect width="100%" height="100%" fill="url(#dz)"/>
510
+ </svg>
511
+
512
+ {['top-left','top-right','bottom-left','bottom-right'].map(c => (
513
+ <Bracket key={c} corner={c} active={drag || hasFile} T={T}/>
514
+ ))}
515
+
516
+ {uploading && (
517
+ <div className="um-scan" style={{ position:'absolute', left:0, right:0, height:2, zIndex:20, pointerEvents:'none', background:T.scanBeam, boxShadow:T.scanGlow }}/>
518
+ )}
519
+
520
+ {/* ── empty / error ── */}
521
+ {!file && (
522
+ <div className={error ? "um-shake" : "um-float"} style={{ position:'absolute', inset:0, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:14, zIndex:10, pointerEvents: 'none' }}>
523
+ <div style={{
524
+ width:56, height:56, borderRadius:18, display:'flex', alignItems:'center', justifyContent:'center',
525
+ background: error ? 'rgba(239,68,68,.1)' : drag ? T.brandGrad : T.iconBg,
526
+ border: drag ? 'none' : `1px solid ${error ? 'transparent' : T.iconBorder}`,
527
+ transform: drag ? 'scale(1.12)' : 'scale(1)',
528
+ boxShadow: drag ? `0 0 28px ${T.acc}55` : 'none', transition:'all .3s',
529
+ }}>
530
+ {error
531
+ ? <AlertTriangle size={26} style={{ color: T.errorText }} />
532
+ : <UploadCloud size={26} style={{ color: drag ? '#fff' : T.acc }}/>
533
+ }
534
+ </div>
535
+ <div style={{ textAlign:'center' }}>
536
+ <p style={{ ...syne, fontSize:16, fontWeight:600, color: error ? T.errorText : T.t1, margin:0, transition: 'color .3s' }}>
537
+ {error ? 'Upload Failed' : drag ? 'Release to drop file' : 'Drag & drop your PDF here'}
538
+ </p>
539
+ <p style={{ ...f, fontSize:14, color: error ? T.errorText : T.t2, marginTop:4, opacity: error ? 0.8 : 1 }}>
540
+ {error ? error : (
541
+ <>or <span style={{ color:T.acc, textDecoration:'underline', textDecorationColor:`${T.acc}55`, cursor:'pointer', pointerEvents: 'auto' }}>browse files</span> · max 50 MB</>
542
+ )}
543
+ </p>
544
+ </div>
545
+ {!error && (
546
+ <div style={{ display:'flex', gap:6 }}>
547
+ {['PDF/A','Scanned','Multi-page','Encrypted'].map(tag => (
548
+ <span key={tag} style={{ ...mono, fontSize:10, padding:'3px 10px', borderRadius:99, background:T.chipBg, border:`1px solid ${T.chipBorder}`, color:T.t2, transition:'all .4s' }}>
549
+ {tag}
550
+ </span>
551
+ ))}
552
+ </div>
553
+ )}
554
+ </div>
555
+ )}
556
+
557
+ {/* ── file loaded ── */}
558
+ {file && (
559
+ <div style={{ position:'absolute', inset:0, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:16, zIndex:10, padding:'0 32px' }}>
560
+ <div style={{ display:'flex', alignItems:'center', gap:14, width:'100%', maxWidth:440 }}>
561
+ <div style={{ width:44, height:44, borderRadius:12, flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', background: done ? T.doneBg : 'rgba(239,68,68,.1)', border: done ? `1px solid ${T.doneBorder}` : '1px solid rgba(239,68,68,.25)', transition:'all .3s' }}>
562
+ {done ? <CheckCircle2 size={20} style={{ color:T.doneText }}/> : <FileText size={20} style={{ color:'#f87171' }}/>}
563
+ </div>
564
+ <div style={{ flex:1, overflow:'hidden' }}>
565
+ <p style={{ ...syne, fontSize:14, fontWeight:600, color:T.fileText, margin:0, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>
566
+ {file.name}
567
+ </p>
568
+ <p style={{ ...mono, fontSize:11, color:T.fileSub, marginTop:3 }}>
569
+ {(file.size/1048576).toFixed(2)} MB · PDF
570
+ </p>
571
+ </div>
572
+ {status === 'idle' && (
573
+ <button onClick={e => { e.stopPropagation(); reset(); }} style={{ width:28, height:28, borderRadius:8, flexShrink:0, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', background:T.clearBg, border:`1px solid ${T.clearBorder}`, color:T.t2, transition:'all .15s' }}
574
+ onMouseEnter={e => { e.currentTarget.style.background='rgba(239,68,68,.1)'; e.currentTarget.style.color='#f87171'; }}
575
+ onMouseLeave={e => { e.currentTarget.style.background=T.clearBg; e.currentTarget.style.color=T.t2; }}>
576
+ <X size={12}/>
577
+ </button>
578
+ )}
579
+ </div>
580
+
581
+ {uploading && (
582
+ <div style={{ width:'100%', maxWidth:440 }}>
583
+ <div style={{ display:'flex', justifyContent:'space-between', marginBottom:8 }}>
584
+ <span style={{ ...f, fontSize:12, color:T.t2, display:'flex', alignItems:'center', gap:6 }}>
585
+ <Loader size={12} className="um-spin" style={{ color:T.acc }}/>
586
+ {getProgressText()}
587
+ </span>
588
+ <span style={{ ...mono, fontSize:11, color:T.acc }}>
589
+ {Math.min(100,Math.round(pct))}%
590
+ </span>
591
+ </div>
592
+ <div style={{ position:'relative', height:4, borderRadius:99, background:T.progressBg, overflow:'hidden' }}>
593
+ <div className="um-shim" style={{ position:'absolute', left:0, top:0, height:'100%', borderRadius:99, background:T.progressFill, width:`${pct}%`, transition:'width .08s linear' }}/>
594
+ </div>
595
+ </div>
596
+ )}
597
+
598
+ {done && (
599
+ <p className="um-fadeR" style={{ ...f, fontSize:13, fontWeight:500, color:T.doneText, display:'flex', alignItems:'center', gap:6 }}>
600
+ <CheckCircle2 size={14}/> Analysis complete — redirecting…
601
+ </p>
602
+ )}
603
+ </div>
604
+ )}
605
+
606
+ <input type="file" ref={fileRef} onChange={e => e.target.files[0] && validateAndPick(e.target.files[0])} accept="application/pdf" style={{ display:'none' }}/>
607
+ </div>
608
+
609
+ <div style={{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:8 }}>
610
+ {STAGES.map(({ icon: Icon, label }, i) => {
611
+ const active = stage >= i;
612
+ const current = stage === i && uploading;
613
+ return (
614
+ <div key={label} style={{ position:'relative', display:'flex', flexDirection:'column', alignItems:'center', gap:6, padding:'12px 8px', borderRadius:14, overflow:'hidden', transition:'all .35s', background: active ? T.stageActiveBg : T.stageBg, border:`1px solid ${active ? T.stageActiveBorder : T.stageBorder}` }}>
615
+ {current && (
616
+ <div style={{ position:'absolute', inset:0, borderRadius:14, background:`radial-gradient(circle at 50% 50%, ${T.acc}22 0%, transparent 70%)` }}/>
617
+ )}
618
+ <Icon size={14} style={{ color: active ? T.acc : T.t4, transition:'color .35s', zIndex:1 }}/>
619
+ <span style={{ ...mono, fontSize:10, fontWeight: 500, letterSpacing:'1px', textTransform:'uppercase', color: active ? T.acc : T.t4, transition:'color .35s', zIndex:1 }}>{label}</span>
620
+ </div>
621
+ );
622
+ })}
623
+ </div>
624
+ </div>
625
+
626
+ <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'18px 32px 24px', background:T.footerBg, borderTop:`1px solid ${T.footerBorder}`, transition:'background .4s, border-color .4s' }}>
627
+ <div style={{ display:'flex', gap:20 }}>
628
+ <span style={{ ...f, display:'flex', alignItems:'center', gap:6, fontSize:13, color:T.t2 }}>
629
+ <ShieldCheck size={14} style={{ color:T.acc }}/> Encrypted
630
+ </span>
631
+ <span style={{ ...f, display:'flex', alignItems:'center', gap:6, fontSize:13, color:T.t2, cursor:'pointer', transition:'color .15s' }}>
632
+ <HelpCircle size={14}/> Help
633
+ </span>
634
+ </div>
635
+
636
+ <div style={{ display:'flex', gap:10 }}>
637
+ <button onClick={reset} style={{ ...f, padding:'10px 20px', borderRadius:12, fontSize:14, fontWeight:500, cursor:'pointer', transition:'all .15s', background:T.clearBg, border:`1px solid ${T.clearBorder}`, color:T.clearText }}
638
+ onMouseEnter={e => e.currentTarget.style.background=T.clearHover} onMouseLeave={e => e.currentTarget.style.background=T.clearBg}>
639
+ Clear
640
+ </button>
641
+ <button onClick={() => file && status === 'idle' && setStatus('uploading')} disabled={!file || status !== 'idle'}
642
+ style={{ ...f, padding:'10px 28px', borderRadius:12, fontSize:14, fontWeight:600, border:'none', transition:'all .2s', cursor: file && status === 'idle' ? 'pointer' : 'not-allowed', background: file && status === 'idle' ? T.analyseGrad : T.disabledBg, color: file && status === 'idle' ? T.analyseText : T.disabledText, boxShadow: file && status === 'idle' ? T.analyseShadow : 'none' }}
643
+ onMouseEnter={e => { if (file && status==='idle') e.currentTarget.style.transform='translateY(-1px)'; }}
644
+ onMouseLeave={e => e.currentTarget.style.transform='none'}>
645
+ {uploading ? 'Scanning…' : done ? 'Done ✓' : 'Analyse →'}
646
+ </button>
647
+ </div>
648
+ </div>
649
+
650
+ </div>
651
+ </div>
652
+ </div>
653
+ </div>
654
+ );
655
+ }
frontend/src/components/Footer/Footer.jsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function Footer() {
2
+ const guidelines = [
3
+ {
4
+ title: "Data Security",
5
+ desc: "All uploaded documents are encrypted and purged post-analysis."
6
+ },
7
+ {
8
+ title: "Acceptable Use",
9
+ desc: "Users must possess legal rights to the documents they analyze."
10
+ },
11
+ {
12
+ title: "AI Limitations",
13
+ desc: "Insights are AI-assisted and do not constitute formal legal advice."
14
+ }
15
+ ];
16
+
17
+ return (
18
+ <footer id="contact" className="font-footer bg-gray-50 dark:bg-[#080605] border-t border-gray-200 dark:border-cyan-900/20 pt-16 pb-8 transition-colors duration-400 relative z-20">
19
+ <div className="max-w-7xl mx-auto px-6">
20
+
21
+ <div className="grid md:grid-cols-3 gap-8 mb-12">
22
+ {guidelines.map((item, index) => (
23
+ <div key={index} className="bg-white dark:bg-[#120e0c] p-6 rounded-2xl border border-gray-200 dark:border-cyan-900/20 transition-colors duration-400 shadow-sm dark:shadow-none">
24
+ <h4 className="text-gray-900 dark:text-cyan-50 font-bold mb-2">{item.title}</h4>
25
+ <p className="text-gray-600 dark:text-cyan-100/60 text-sm leading-relaxed">{item.desc}</p>
26
+ </div>
27
+ ))}
28
+ </div>
29
+
30
+ <div className="flex flex-col md:flex-row items-center justify-between pt-8 border-t border-gray-200 dark:border-cyan-900/20">
31
+ <div className="flex items-center gap-2 mb-4 md:mb-0">
32
+ <span className="text-gray-900 dark:text-cyan-50 font-bold tracking-tight text-sm uppercase">PolicyLens</span>
33
+ </div>
34
+
35
+ <p className="text-gray-500 dark:text-cyan-100/40 text-sm font-medium">
36
+ © 2026 PolicyLens AI. All rights reserved.
37
+ </p>
38
+ </div>
39
+
40
+ </div>
41
+ </footer>
42
+ );
43
+ }
frontend/src/components/Hero/Hero.jsx ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ArrowRight, FileText, Loader, CheckCircle2, AlertCircle } from 'lucide-react';
2
+ import { useEffect, useState } from 'react';
3
+
4
+ const StarField = () => {
5
+ const [stars, setStars] = useState([]);
6
+
7
+ useEffect(() => {
8
+ const newStars = Array.from({ length: 40 }).map((_, i) => ({
9
+ id: i,
10
+ left: `${Math.random() * 100}%`,
11
+ top: `${Math.random() * 100}%`,
12
+ size: `${Math.random() * 3 + 1}px`,
13
+ animationDelay: `${Math.random() * 4}s`,
14
+ opacity: Math.random() * 0.5 + 0.1
15
+ }));
16
+ setStars(newStars);
17
+ }, []);
18
+
19
+ return (
20
+ <div className="absolute inset-0 z-0 overflow-hidden pointer-events-none hidden dark:block transition-opacity duration-1000">
21
+ {stars.map((star) => (
22
+ <div
23
+ key={star.id}
24
+ className="absolute rounded-full bg-cyan-100 animate-twinkle"
25
+ style={{
26
+ left: star.left,
27
+ top: star.top,
28
+ width: star.size,
29
+ height: star.size,
30
+ animationDelay: star.animationDelay,
31
+ opacity: star.opacity,
32
+ boxShadow: `0 0 ${Math.random() * 4 + 2}px rgba(34, 211, 238, 0.8)`
33
+ }}
34
+ />
35
+ ))}
36
+ </div>
37
+ );
38
+ };
39
+
40
+ export default function Hero({ onGetStarted }) {
41
+ return (
42
+ <section id="hero" className="relative min-h-screen flex items-center pt-24 pb-12 overflow-hidden">
43
+
44
+ {/* --- BACKGROUND ATMOSPHERE --- */}
45
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[600px] bg-brand-teal/5 dark:bg-transparent rounded-full blur-[120px] pointer-events-none transition-colors duration-700 z-0" />
46
+ <StarField />
47
+ <div className="absolute top-[-10%] left-1/2 -translate-x-1/2 w-[1000px] h-[800px] bg-cyan-500/0 dark:bg-cyan-500/10 rounded-[100%] blur-[150px] pointer-events-none transition-colors duration-1000 z-0" />
48
+ <div className="absolute top-1/4 right-[-10%] w-[800px] h-[800px] bg-white/0 dark:bg-[#e0f2fe]/5 rounded-[100%] blur-[120px] pointer-events-none transition-colors duration-1000 z-0" />
49
+ <div className="absolute bottom-0 left-0 right-0 h-64 bg-gradient-to-t from-transparent dark:from-[#1a1210]/40 to-transparent pointer-events-none z-0" />
50
+
51
+ {/* --- CONTENT --- */}
52
+ <div className="max-w-7xl mx-auto px-6 w-full grid lg:grid-cols-2 gap-16 items-center relative z-10">
53
+
54
+ {/* LEFT SIDE: Copy */}
55
+ <div className="max-w-xl text-center lg:text-left z-20">
56
+ <h1 className="text-6xl sm:text-7xl lg:text-[80px] font-bold leading-[1.05] tracking-tight mb-6 text-gray-900 dark:text-[#f0f9ff] transition-colors duration-400">
57
+ One-click <br />
58
+ for <span className="text-gray-400 dark:text-cyan-200/80">Policy</span> <br />
59
+ Defense.
60
+ </h1>
61
+
62
+ <p className="font-nav text-lg font-light text-gray-600 dark:text-cyan-100/60 leading-relaxed mb-10 transition-colors duration-400 mx-auto lg:mx-0 max-w-md">
63
+ Dive into automated document analysis, where innovative AI technology meets legal expertise. Get answers instantly.
64
+ </p>
65
+
66
+ <button
67
+ onClick={onGetStarted}
68
+ className="bg-brand-teal hover:bg-brand-teal-hover dark:bg-[#e0f2fe] dark:hover:bg-white text-white dark:text-[#0c0908] font-bold px-8 py-3.5 rounded-full transition-all flex items-center justify-center gap-2.5 mx-auto lg:mx-0 shadow-lg shadow-brand-teal/20 dark:shadow-cyan-500/20"
69
+ >
70
+ Get Started
71
+ <ArrowRight size={18} strokeWidth={2.5} />
72
+ </button>
73
+ </div>
74
+
75
+ {/* RIGHT SIDE: Animated UI & FLOATING PDF DOCUMENTS */}
76
+ <div className="relative h-[650px] flex items-center justify-center w-full">
77
+
78
+ {/* Main Back Card */}
79
+ <div className="absolute right-16 top-28 z-10 w-72 bg-white dark:bg-[#15100e]/80 backdrop-blur-md border border-gray-200 dark:border-cyan-900/30 rounded-3xl p-6 shadow-2xl animate-float-delayed transition-colors duration-400">
80
+ <div className="flex items-center gap-3 mb-6">
81
+ <div className="w-8 h-8 rounded bg-brand-teal dark:bg-cyan-950 flex items-center justify-center text-white dark:text-cyan-400 text-xs font-bold">P</div>
82
+ <div>
83
+ <div className="text-sm font-bold text-gray-900 dark:text-cyan-50">Compliance Check</div>
84
+ <div className="text-xs text-gray-500 dark:text-cyan-200/50">policy_v2.pdf</div>
85
+ </div>
86
+ </div>
87
+ <div className="text-xs text-gray-500 dark:text-cyan-200/50 mb-1">Total Risks Found</div>
88
+ <div className="text-3xl font-bold tracking-tight text-gray-900 dark:text-cyan-100 mb-4">3 Warnings</div>
89
+ </div>
90
+
91
+ {/* Main Front Card */}
92
+ <div className="absolute right-40 top-44 z-30 w-64 bg-brand-teal dark:bg-[#0c0908]/90 backdrop-blur-xl border border-transparent dark:border-cyan-800/40 rounded-3xl p-6 shadow-2xl dark:shadow-cyan-900/20 animate-float-smooth transition-colors duration-400">
93
+ <div className="text-white/80 dark:text-cyan-200/60 text-xs mb-1">Status</div>
94
+ <div className="text-white dark:text-cyan-50 text-xl font-bold mb-6">Scan Complete</div>
95
+
96
+ <div className="space-y-3 mb-6">
97
+ <div className="bg-white/10 dark:bg-cyan-900/30 p-2.5 rounded-lg border border-white/5 dark:border-cyan-800/50">
98
+ <div className="flex items-center gap-2 mb-1">
99
+ <div className="w-1.5 h-1.5 rounded-full bg-green-400" />
100
+ <span className="text-white dark:text-cyan-100 text-xs font-semibold">Key Finding</span>
101
+ </div>
102
+ <p className="text-white/80 dark:text-cyan-100/70 text-[10px] leading-relaxed">
103
+ Termination clause extends to 90 days. standard is 30 days.
104
+ </p>
105
+ </div>
106
+
107
+ <div className="bg-white/10 dark:bg-cyan-900/30 p-2.5 rounded-lg border border-white/5 dark:border-cyan-800/50">
108
+ <div className="flex items-center gap-2 mb-1">
109
+ <div className="w-1.5 h-1.5 rounded-full bg-blue-400" />
110
+ <span className="text-white dark:text-cyan-100 text-xs font-semibold">Obligation</span>
111
+ </div>
112
+ <p className="text-white/80 dark:text-cyan-100/70 text-[10px] leading-relaxed">
113
+ Quarterly audits required starting Q3 2026.
114
+ </p>
115
+ </div>
116
+ </div>
117
+
118
+ <div className="flex justify-between items-center border-t border-white/10 dark:border-cyan-900/40 pt-4 mt-2">
119
+ <div className="text-white/90 dark:text-cyan-100/80 text-[11px] font-medium">Auto-Sync Insights</div>
120
+ <div className="w-8 h-4 bg-white/30 dark:bg-cyan-700/50 rounded-full relative">
121
+ <div className="absolute right-0.5 top-0.5 w-3 h-3 bg-white rounded-full shadow" />
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ {/* PDF 1: Top Left */}
127
+ <div className="absolute left-0 top-10 z-20 w-44 bg-white/95 dark:bg-[#15100e]/95 backdrop-blur-md border border-gray-100 dark:border-cyan-900/30 p-4 rounded-2xl shadow-xl animate-float-fast transition-colors duration-400 transform -rotate-3">
128
+ <div className="flex items-center justify-between mb-3">
129
+ <div className="flex items-center gap-2">
130
+ <FileText className="text-blue-500 dark:text-blue-400" size={16} />
131
+ <span className="text-[10px] font-bold text-gray-800 dark:text-gray-200">Legal_v1.pdf</span>
132
+ </div>
133
+ <span className="text-[9px] font-medium text-blue-500 flex items-center gap-1">
134
+ <Loader size={10} className="animate-spin" />
135
+ Parsing...
136
+ </span>
137
+ </div>
138
+ <div className="space-y-2">
139
+ <div className="flex items-center gap-2">
140
+ <div className="h-1 w-full bg-gray-200 dark:bg-slate-700 rounded-full overflow-hidden">
141
+ <div className="h-full w-[45%] bg-blue-500 dark:bg-blue-400 rounded-full" />
142
+ </div>
143
+ <span className="text-[8px] text-gray-500">45%</span>
144
+ </div>
145
+ <div className="h-1 w-4/6 bg-gray-100 dark:bg-slate-800 rounded-full" />
146
+ </div>
147
+ </div>
148
+
149
+ {/* PDF 2: Far Right Edge */}
150
+ <div className="absolute right-[-10px] top-1/4 z-0 w-36 bg-white/90 dark:bg-[#111111]/90 backdrop-blur-md border border-gray-100 dark:border-cyan-900/20 p-3 rounded-xl shadow-lg animate-float-slow transition-colors duration-400 transform rotate-6">
151
+ <div className="flex justify-between items-center mb-2">
152
+ <span className="text-[9px] font-bold text-gray-700 dark:text-cyan-100">Summary.pdf</span>
153
+ <span className="text-[8px] text-brand-teal dark:text-cyan-500">Extracting...</span>
154
+ </div>
155
+ <div className="space-y-1.5 mt-2">
156
+ <div className="text-[7px] text-gray-400 dark:text-gray-500">Locating key clauses</div>
157
+ <div className="h-1 w-full bg-brand-teal/20 dark:bg-cyan-900/50 rounded-full relative overflow-hidden">
158
+ <div className="absolute top-0 left-0 h-full w-[70%] bg-brand-teal dark:bg-cyan-600 rounded-full" />
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ {/* PDF 3: Middle Left */}
164
+ <div className="absolute left-[-90px] top-1/2 -translate-y-1/2 z-40 w-48 bg-white/95 dark:bg-[#15100e]/95 backdrop-blur-md border border-green-100 dark:border-green-900/30 p-4 rounded-2xl shadow-2xl animate-float-delayed transition-colors duration-400">
165
+ <div className="flex items-center justify-between mb-2">
166
+ <div className="flex items-center gap-1.5">
167
+ <CheckCircle2 className="text-green-500 dark:text-green-400" size={14} />
168
+ <span className="text-xs font-bold text-gray-900 dark:text-cyan-50">Contract_Final</span>
169
+ </div>
170
+ <span className="text-[9px] font-bold text-green-600 bg-green-50 dark:bg-green-900/30 px-1.5 py-0.5 rounded">Safe</span>
171
+ </div>
172
+ <div className="mt-3">
173
+ <div className="text-[9px] text-gray-500 dark:text-gray-400 mb-1.5">No liabilities detected.</div>
174
+ <div className="flex gap-1">
175
+ <div className="h-6 w-1/3 bg-green-50 dark:bg-green-900/20 rounded border border-green-100 dark:border-green-900/30 flex items-center justify-center text-[8px] text-green-600 dark:text-green-400 font-medium">Clear</div>
176
+ <div className="h-6 w-1/3 bg-green-50 dark:bg-green-900/20 rounded border border-green-100 dark:border-green-900/30 flex items-center justify-center text-[8px] text-green-600 dark:text-green-400 font-medium">Valid</div>
177
+ <div className="h-6 w-1/3 bg-green-50 dark:bg-green-900/20 rounded border border-green-100 dark:border-green-900/30 flex items-center justify-center text-[8px] text-green-600 dark:text-green-400 font-medium">Signed</div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ {/* PDF 4: Bottom Right */}
183
+ <div className="absolute right-0 bottom-24 z-40 w-40 bg-white/95 dark:bg-[#120e0c]/95 backdrop-blur-md border border-red-100 dark:border-red-900/30 p-4 rounded-2xl shadow-xl animate-float-smooth transition-colors duration-400 transform -rotate-6">
184
+ <div className="flex items-center justify-between mb-2 pb-2 border-b border-gray-100 dark:border-slate-800">
185
+ <div className="flex items-center gap-1.5">
186
+ <AlertCircle className="text-red-500" size={14} />
187
+ <span className="text-[10px] font-bold text-gray-800 dark:text-gray-200">Risk_Report</span>
188
+ </div>
189
+ </div>
190
+ <div className="mt-2 space-y-1">
191
+ <div className="text-[9px] text-red-500 font-medium">! Missing signatures</div>
192
+ <div className="text-[9px] text-red-500 font-medium">! Clause 3.1 conflict</div>
193
+ </div>
194
+ </div>
195
+
196
+ {/* PDF 5: Bottom Center/Left */}
197
+ <div className="absolute left-1/4 bottom-10 z-20 w-40 bg-white/90 dark:bg-[#15100e]/90 backdrop-blur-md border border-gray-100 dark:border-cyan-900/30 p-3.5 rounded-xl shadow-lg animate-float-fast transition-colors duration-400 transform rotate-3">
198
+ <div className="absolute inset-0 bg-white/50 dark:bg-[#15100e]/50 border border-gray-100 dark:border-cyan-900/30 rounded-xl transform -rotate-6 -z-10 translate-y-1 translate-x-1" />
199
+ <div className="flex items-center justify-between mb-2">
200
+ <div className="flex items-center gap-1.5">
201
+ <FileText className="text-purple-500 dark:text-purple-400" size={12} />
202
+ <span className="text-[10px] font-bold text-gray-700 dark:text-cyan-100">NDA_Template</span>
203
+ </div>
204
+ <span className="text-[8px] text-purple-500 dark:text-purple-400 font-medium">Reviewing</span>
205
+ </div>
206
+ <div className="space-y-1.5">
207
+ <div className="text-[8px] text-gray-600 dark:text-gray-400">Confidentiality terms</div>
208
+ <div className="flex items-center gap-1.5">
209
+ <div className="h-1 flex-1 bg-purple-100 dark:bg-purple-900/30 rounded-full overflow-hidden">
210
+ <div className="h-full w-[60%] bg-purple-500 dark:bg-purple-400 rounded-full" />
211
+ </div>
212
+ <span className="text-[8px] text-gray-500">60%</span>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ {/* PDF 6: Top Center */}
218
+ <div className="absolute left-1/3 top-16 z-0 w-32 bg-white/60 dark:bg-[#15100e]/60 backdrop-blur-sm border border-gray-100 dark:border-cyan-900/20 p-3 rounded-lg shadow animate-float-smooth transition-colors duration-400 opacity-70">
219
+ <div className="flex items-center justify-between mb-2">
220
+ <span className="text-[9px] font-bold text-gray-600 dark:text-gray-300">Index.pdf</span>
221
+ <span className="text-[7px] text-gray-400">Draft</span>
222
+ </div>
223
+ <div className="space-y-1">
224
+ <div className="text-[7px] text-gray-500 dark:text-gray-400">Table of Contents</div>
225
+ <div className="h-0.5 w-full bg-gray-300 dark:bg-slate-600 rounded-full" />
226
+ <div className="h-0.5 w-3/4 bg-gray-300 dark:bg-slate-600 rounded-full" />
227
+ <div className="h-0.5 w-2/3 bg-gray-300 dark:bg-slate-600 rounded-full" />
228
+ </div>
229
+ </div>
230
+
231
+ </div>
232
+ </div>
233
+ </section>
234
+ );
235
+ }
frontend/src/components/Navbar/Navbar.jsx ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Moon, Sun, X, Info, Mail } from 'lucide-react';
3
+ import logo from '../../assets/pla.png';
4
+ // Adding our custom fonts specifically for the modals
5
+ const FONT_LINK = 'https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap';
6
+
7
+ export default function Navbar({ isDark, toggleTheme, hideNavLinks }) {
8
+ const [activeModal, setActiveModal] = useState(null);
9
+
10
+ useEffect(() => {
11
+ const link = Object.assign(document.createElement('link'), { rel: 'stylesheet', href: FONT_LINK });
12
+ document.head.appendChild(link);
13
+ return () => link.remove();
14
+ }, []);
15
+
16
+ // Scrolls to the hero section; falls back to top if #hero not found
17
+ const handleHomeClick = (e) => {
18
+ e.preventDefault();
19
+ const hero = document.getElementById('hero');
20
+ if (hero) {
21
+ hero.scrollIntoView({ behavior: 'smooth' });
22
+ } else {
23
+ window.scrollTo({ top: 0, behavior: 'smooth' });
24
+ }
25
+ setActiveModal(null);
26
+ };
27
+
28
+ const closeModal = () => setActiveModal(null);
29
+
30
+ return (
31
+ <>
32
+ <nav className="w-full h-20 px-6 md:px-10 flex items-center justify-between bg-white dark:bg-[#0c0908] border-b border-gray-100 dark:border-[#231a15] transition-colors duration-300 z-40 relative">
33
+
34
+ {/* LEFT: Logo */}
35
+ <div className="flex items-center gap-3 cursor-pointer z-10" onClick={handleHomeClick}>
36
+ <img
37
+ src={logo}
38
+ alt="PolicyLens"
39
+ className="h-8 w-auto object-contain rounded-lg"
40
+ />
41
+ <span className="text-xl font-bold tracking-tight text-gray-900 dark:text-cyan-50">
42
+ PolicyLens
43
+ </span>
44
+ </div>
45
+
46
+
47
+ {/* CENTER: Nav Links - Conditionally Rendered */}
48
+ {!hideNavLinks && (
49
+ <div className="absolute left-1/2 -translate-x-1/2 hidden md:flex items-center gap-10 font-nav">
50
+ <button
51
+ onClick={handleHomeClick}
52
+ className="text-gray-500 dark:text-cyan-100/50 hover:text-brand-teal dark:hover:text-cyan-200 font-semibold text-[13px] tracking-[0.2em] uppercase transition-colors"
53
+ >
54
+ Home
55
+ </button>
56
+ <button
57
+ onClick={() => setActiveModal('about')}
58
+ className="text-gray-500 dark:text-cyan-100/50 hover:text-brand-teal dark:hover:text-cyan-200 font-semibold text-[13px] tracking-[0.2em] uppercase transition-colors"
59
+ >
60
+ About
61
+ </button>
62
+ <button
63
+ onClick={() => setActiveModal('contact')}
64
+ className="text-gray-500 dark:text-cyan-100/50 hover:text-brand-teal dark:hover:text-cyan-200 font-semibold text-[13px] tracking-[0.2em] uppercase transition-colors"
65
+ >
66
+ Contact
67
+ </button>
68
+ </div>
69
+ )}
70
+
71
+ {/* RIGHT: Theme Toggle Button */}
72
+ <div className="flex items-center gap-4 z-10">
73
+ <button
74
+ onClick={toggleTheme}
75
+ className="p-2.5 rounded-xl bg-gray-50 dark:bg-[#1a1310] border border-gray-200 dark:border-[#2a1f1a] text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
76
+ aria-label="Toggle Theme"
77
+ >
78
+ {isDark ? <Sun size={18} /> : <Moon size={18} />}
79
+ </button>
80
+ </div>
81
+
82
+ </nav>
83
+
84
+ {/* ── MODALS OVERLAY ── */}
85
+ {activeModal && (
86
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm transition-opacity" onClick={closeModal}>
87
+
88
+ {/* ABOUT MODAL */}
89
+ {activeModal === 'about' && (
90
+ <div
91
+ className="relative w-full max-w-md p-8 rounded-3xl border shadow-2xl bg-white dark:bg-[#15100d] border-gray-200 dark:border-[#2a1f1a] animate-[slideUp_0.3s_ease-out]"
92
+ onClick={e => e.stopPropagation()}
93
+ >
94
+ <button onClick={closeModal} className="absolute top-5 right-5 p-2 rounded-full hover:bg-gray-100 dark:hover:bg-white/5 transition-colors text-gray-500 dark:text-gray-400">
95
+ <X size={20} />
96
+ </button>
97
+
98
+ <div className="flex items-center gap-4 mb-6">
99
+ <div className="p-3 rounded-2xl bg-cyan-50 dark:bg-cyan-900/20 text-cyan-600 dark:text-cyan-400">
100
+ <Info size={24} />
101
+ </div>
102
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
103
+ About PolicyLens AI
104
+ </h2>
105
+ </div>
106
+
107
+ <p className="leading-relaxed text-gray-600 dark:text-gray-300" style={{ fontFamily: "'Outfit', sans-serif", fontSize: '15px' }}>
108
+ PolicyLens is an advanced AI-powered legal document analyzer. Powered by IRIS, our proprietary engine, it scans complex contracts, NDAs, and policy documents to instantly identify high-risk clauses, indemnification liabilities, and unusual termination terms.
109
+ </p>
110
+ </div>
111
+ )}
112
+
113
+ {/* CONTACT MODAL */}
114
+ {activeModal === 'contact' && (
115
+ <div
116
+ className="relative w-full max-w-md p-8 rounded-3xl border shadow-2xl bg-white dark:bg-[#15100d] border-gray-200 dark:border-[#2a1f1a] animate-[slideUp_0.3s_ease-out]"
117
+ onClick={e => e.stopPropagation()}
118
+ >
119
+ <button onClick={closeModal} className="absolute top-5 right-5 p-2 rounded-full hover:bg-gray-100 dark:hover:bg-white/5 transition-colors text-gray-500 dark:text-gray-400">
120
+ <X size={20} />
121
+ </button>
122
+
123
+ <div className="flex items-center gap-4 mb-6">
124
+ <div className="p-3 rounded-2xl bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400">
125
+ <Mail size={24} />
126
+ </div>
127
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
128
+ Contact Us
129
+ </h2>
130
+ </div>
131
+
132
+ <div className="flex flex-col gap-3">
133
+ {[
134
+ { dept: 'General Inquiries', email: 'hello@policylens.ai' },
135
+ { dept: 'Enterprise Sales', email: 'sales@policylens.ai' },
136
+ { dept: 'Technical Support', email: 'support@policylens.ai' },
137
+ { dept: 'Legal Team', email: 'legal@policylens.ai' }
138
+ ].map((contact, index) => (
139
+ <div key={index} className="flex flex-col p-3.5 rounded-xl border bg-gray-50 border-gray-100 dark:bg-[#1a1310] dark:border-[#2a1f1a]">
140
+ <span className="text-xs uppercase tracking-wider text-gray-500 dark:text-gray-500 mb-1" style={{ fontFamily: "'Outfit', sans-serif", fontWeight: 600 }}>
141
+ {contact.dept}
142
+ </span>
143
+ <a href={`mailto:${contact.email}`} className="font-medium text-brand-teal dark:text-cyan-400 hover:underline transition-all" style={{ fontFamily: "'Outfit', sans-serif" }}>
144
+ {contact.email}
145
+ </a>
146
+ </div>
147
+ ))}
148
+ </div>
149
+ </div>
150
+ )}
151
+
152
+ </div>
153
+ )}
154
+
155
+ {/* Animation class for the modals */}
156
+ <style>{`
157
+ @keyframes slideUp {
158
+ from { opacity: 0; transform: translateY(10px); }
159
+ to { opacity: 1; transform: translateY(0); }
160
+ }
161
+ `}</style>
162
+ </>
163
+ );
164
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;700&family=Space+Grotesk:wght@400;600&display=swap');
2
+ @import "tailwindcss";
3
+
4
+ @custom-variant dark (&:where(.dark, .dark *));
5
+
6
+ @theme {
7
+ --font-sans: 'Inter', sans-serif;
8
+ --font-nav: 'Outfit', sans-serif;
9
+ --font-footer: 'Space Grotesk', sans-serif;
10
+
11
+ --color-brand-teal: #2A7C76;
12
+ --color-brand-teal-hover: #236560;
13
+
14
+ --animate-float-smooth: floatSmooth 8s ease-in-out infinite;
15
+ --animate-float-delayed: floatSmooth 8s ease-in-out infinite 4s;
16
+ --animate-float-fast: floatSmooth 5s ease-in-out infinite 1s;
17
+ --animate-float-slow: floatSmooth 10s ease-in-out infinite 2s;
18
+ --animate-twinkle: twinkle 4s ease-in-out infinite alternate;
19
+
20
+ @keyframes floatSmooth {
21
+ 0%, 100% { transform: translateY(0px); }
22
+ 50% { transform: translateY(-16px); }
23
+ }
24
+
25
+ @keyframes twinkle {
26
+ 0% { opacity: 0.2; transform: scale(0.8); }
27
+ 100% { opacity: 0.8; transform: scale(1.2); }
28
+ }
29
+ }
30
+
31
+ html {
32
+ scroll-behavior: smooth;
33
+ }
34
+
35
+ body {
36
+ /* Light mode stays exactly as it was */
37
+ background-color: #FAFAFA;
38
+ color: #111827;
39
+ transition: background-color 0.6s ease, color 0.6s ease;
40
+ }
41
+
42
+ /*
43
+ DARK MODE BASE
44
+ A very deep, rich black-brown/espresso base.
45
+ */
46
+ .dark body {
47
+ background-color: #0c0908;
48
+ color: #f8fafc;
49
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), tailwindcss()],
7
+ })
rag_engine/main.py CHANGED
@@ -4,7 +4,10 @@ Usage:
4
  python rag_engine/main.py --dry-run
5
  python rag_engine/main.py --pdf path/to/policy.pdf --policy-id POL-001
6
  """
7
-
 
 
 
8
  from __future__ import annotations
9
 
10
  import argparse
 
4
  python rag_engine/main.py --dry-run
5
  python rag_engine/main.py --pdf path/to/policy.pdf --policy-id POL-001
6
  """
7
+ import sys
8
+ import io
9
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
10
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
11
  from __future__ import annotations
12
 
13
  import argparse
rag_engine/services/ingestion_service.py CHANGED
@@ -68,8 +68,7 @@ class IngestionService:
68
  # Step 4 — embed all chunks
69
  texts = [text for text, _ in chunks]
70
  metadatas = [
71
- meta.__dict__ if hasattr(meta, "__dict__") and not hasattr(meta, "model_dump")
72
- else meta.model_dump()
73
  for _, meta in chunks
74
  ]
75
  logger.info("Embedding %d chunks...", len(texts))
 
68
  # Step 4 — embed all chunks
69
  texts = [text for text, _ in chunks]
70
  metadatas = [
71
+ meta.to_supabase_dict() if hasattr(meta, "to_supabase_dict") else meta.model_dump()
 
72
  for _, meta in chunks
73
  ]
74
  logger.info("Embedding %d chunks...", len(texts))
rag_engine/vector_store/supabase_store.py CHANGED
@@ -35,9 +35,14 @@ class SupabaseVectorStore(BaseVectorStore):
35
  for i in range(0, len(rows), _BATCH_SIZE):
36
  batch = rows[i : i + _BATCH_SIZE]
37
  batch_num = i // _BATCH_SIZE + 1
38
- logger.info("Inserting batch %d/%d (%d rows)", batch_num, total, len(batch))
39
- self._client.table(self._table).insert(batch).execute()
40
- logger.info("Stored %d chunks", len(chunks))
 
 
 
 
 
41
 
42
  # ------------------------------------------------------------------ #
43
  # read
 
35
  for i in range(0, len(rows), _BATCH_SIZE):
36
  batch = rows[i : i + _BATCH_SIZE]
37
  batch_num = i // _BATCH_SIZE + 1
38
+ logger.info("Inserting batch %d/%d (%d rows) to Supabase...", batch_num, total, len(batch))
39
+ try:
40
+ res = self._client.table(self._table).insert(batch).execute()
41
+ logger.info("Batch %d stored successfully", batch_num)
42
+ except Exception as e:
43
+ logger.error("FAILED to store batch %d: %s", batch_num, str(e))
44
+ raise e
45
+ logger.info("Successfully stored %d chunks in Supabase", len(chunks))
46
 
47
  # ------------------------------------------------------------------ #
48
  # read