Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- .gitattributes +17 -0
- .gitignore +0 -0
- Dockerfile +1 -1
- api/main.py +11 -15
- backend/src/routes/ingest.js +21 -5
- frontend/.gitignore +24 -0
- frontend/eslint.config.js +29 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +30 -0
- frontend/public/vite.svg +1 -0
- frontend/src/App.css +42 -0
- frontend/src/App.jsx +127 -0
- frontend/src/assets/pla.png +3 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/components/About/About.jsx +79 -0
- frontend/src/components/Auth/Login.jsx +501 -0
- frontend/src/components/Dashboard/Chatbot.jsx +343 -0
- frontend/src/components/Dashboard/Dboard.jsx +752 -0
- frontend/src/components/Dashboard/UploadModal.jsx +655 -0
- frontend/src/components/Footer/Footer.jsx +43 -0
- frontend/src/components/Hero/Hero.jsx +235 -0
- frontend/src/components/Navbar/Navbar.jsx +164 -0
- frontend/src/index.css +49 -0
- frontend/src/main.jsx +10 -0
- frontend/vite.config.js +7 -0
- rag_engine/main.py +4 -1
- rag_engine/services/ingestion_service.py +1 -2
- rag_engine/vector_store/supabase_store.py +8 -3
.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-
|
| 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 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
-
// 6. Return
|
| 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 Python — wait 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
|
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.
|
| 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 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|