Punit1 commited on
Commit
4a41445
Β·
1 Parent(s): 939c0c0

feat: implement registration flow, backend auth services, and multi-agent response synthesizer

Browse files
agents/synthesizer.py CHANGED
@@ -16,13 +16,15 @@ from agents.state import CopilotState
16
  logger = structlog.get_logger(__name__)
17
 
18
  SYNTHESIZER_SYSTEM_PROMPT = """You are an Enterprise AI Copilot Assistant.
19
- Synthesize a clear, concise, and professional answer to the user's query based on the context and conversation history.
20
-
21
- Strict Response Guidelines:
22
- 1. **Be Concise & Direct**: Answer the user's exact question in the VERY FIRST sentence. Do not add long generic intros or repeat capability lists unless explicitly asked.
23
- 2. **Match Response Length**: For simple or short questions (e.g., "tell my name", "hi", "what is X"), give a short 1-2 sentence response. For complex analysis or document queries, provide detailed markdown formatting.
24
- 3. **Use Conversation History**: Pay close attention to previous turns in CONVERSATION HISTORY (e.g., if the user previously stated their name, preferences, or topics, remember and use them).
25
- 4. **Grounding**: Ground document answers strictly in RETRIEVED CONTEXT. Cite source documents clearly.
 
 
26
  """
27
 
28
  # ── Built-in greeting responses for initial turn ────────────
@@ -38,7 +40,6 @@ def _extract_name_from_history(history: list) -> str | None:
38
  for msg in history:
39
  if msg.get("role") == "user":
40
  content = msg.get("content", "")
41
- # Match "my name is X", "i am X", "im X", "call me X"
42
  match = re.search(r"\b(?:my name is|i am|i'm|call me)\s+([A-Za-z]+)", content, re.IGNORECASE)
43
  if match:
44
  name = match.group(1).capitalize()
@@ -52,17 +53,17 @@ def _build_chunk_summary(chunks: list) -> str:
52
  if not chunks:
53
  return ""
54
 
55
- summary_parts = ["πŸ“„ **Here's what I found in your documents:**\n"]
56
  for i, c in enumerate(chunks[:5], 1):
57
- doc_name = c.get("document_name", "Unknown Document")
58
  page = c.get("page", "N/A")
59
  text = c.get("text", "")
60
- if len(text) > 300:
61
- text = text[:300] + "..."
62
- summary_parts.append(f"**{i}. {doc_name}** (Page {page})")
63
  summary_parts.append(f"> {text}\n")
64
 
65
- summary_parts.append("\n---\n*πŸ’‘ Tip: Set up your Groq API key in `.env` for full AI-powered analysis.*")
66
  return "\n".join(summary_parts)
67
 
68
 
@@ -82,12 +83,10 @@ async def synthesizer_node(state: CopilotState) -> CopilotState:
82
  # Format context for prompt
83
  context_str = ""
84
  if chunks:
85
- context_str = "--- RETRIEVED CONTEXT ---\n"
86
  for i, c in enumerate(chunks, 1):
87
- context_str += f"[{i}] File: {c.get('document_name')} (Page {c.get('page', 'N/A')})\n"
88
- context_str += f"Content: {c.get('text')}\n\n"
89
- elif routing != "direct":
90
- context_str = "No specific internal document context was found for this query.\n"
91
 
92
  # Format history
93
  history_str = ""
 
16
  logger = structlog.get_logger(__name__)
17
 
18
  SYNTHESIZER_SYSTEM_PROMPT = """You are an Enterprise AI Copilot Assistant.
19
+ Your primary job is to analyze and synthesize accurate answers using the provided context, web search results, or general knowledge.
20
+
21
+ CRITICAL RULES FOR RESPONSES:
22
+ 1. **FULL DOCUMENT ACCESS**: When RETRIEVED CONTEXT is present, you HAVE FULL ACCESS to the user's uploaded file excerpts. NEVER state or claim that the document is "not publicly available", "private", or "unavailable".
23
+ 2. **NO NEGATIVE DISCLAIMERS**: NEVER say or output phrases like "There is no related content", "No specific internal document context was found", or "I could not find information" when answering general queries or web search questions. Simply answer the query directly and helpfully.
24
+ 3. **SUMMARIZATION**: If the user asks "Tell me about the contents of [document_name]" or asks what is in a document, summarize the key concepts, sections, and topics present in the Excerpts.
25
+ 4. **CONCISENESS**: Be direct. Answer the user's question in the very first paragraph. Use markdown bullet points and bold headers for clarity.
26
+ 5. **CITATIONS**: Reference document names, page numbers, or web sources clearly.
27
+ 6. **CONVERSATION MEMORY**: Use the CONVERSATION HISTORY to remember details the user shared (such as their name or previous topics).
28
  """
29
 
30
  # ── Built-in greeting responses for initial turn ────────────
 
40
  for msg in history:
41
  if msg.get("role") == "user":
42
  content = msg.get("content", "")
 
43
  match = re.search(r"\b(?:my name is|i am|i'm|call me)\s+([A-Za-z]+)", content, re.IGNORECASE)
44
  if match:
45
  name = match.group(1).capitalize()
 
53
  if not chunks:
54
  return ""
55
 
56
+ summary_parts = ["πŸ“„ **Here is what I extracted from your document:**\n"]
57
  for i, c in enumerate(chunks[:5], 1):
58
+ doc_name = c.get("document_name", "Uploaded Document")
59
  page = c.get("page", "N/A")
60
  text = c.get("text", "")
61
+ if len(text) > 350:
62
+ text = text[:350] + "..."
63
+ summary_parts.append(f"**Section {i} β€” {doc_name}** (Page {page})")
64
  summary_parts.append(f"> {text}\n")
65
 
66
+ summary_parts.append("\n---\n*πŸ’‘ Tip: Set up your free Groq API key in `.env` for AI-powered synthesis.*")
67
  return "\n".join(summary_parts)
68
 
69
 
 
83
  # Format context for prompt
84
  context_str = ""
85
  if chunks:
86
+ context_str = "--- CONTEXT & SEARCH RESULTS ---\n"
87
  for i, c in enumerate(chunks, 1):
88
+ context_str += f"[{i}] Source: {c.get('document_name')} (Page {c.get('page', 'N/A')})\n"
89
+ context_str += f"Excerpt: {c.get('text')}\n\n"
 
 
90
 
91
  # Format history
92
  history_str = ""
backend/app/api/v1/documents.py CHANGED
@@ -125,6 +125,16 @@ async def upload_document(
125
  if doc_type not in valid_types:
126
  doc_type = "general"
127
 
 
 
 
 
 
 
 
 
 
 
128
  # Save file to tenant disk location
129
  upload_dir = os.path.join(settings.UPLOAD_DIR, current_user.tenant_id)
130
  os.makedirs(upload_dir, exist_ok=True)
 
125
  if doc_type not in valid_types:
126
  doc_type = "general"
127
 
128
+ # Enforce RBAC permissions on document category upload
129
+ if current_user.role == UserRole.EMPLOYEE and doc_type in {"hr", "finance", "legal"}:
130
+ raise HTTPException(
131
+ status_code=status.HTTP_403_FORBIDDEN,
132
+ detail={
133
+ "error": "permission_denied",
134
+ "message": f"Standard employees cannot upload restricted '{doc_type}' documents. Admin/Manager approval required.",
135
+ },
136
+ )
137
+
138
  # Save file to tenant disk location
139
  upload_dir = os.path.join(settings.UPLOAD_DIR, current_user.tenant_id)
140
  os.makedirs(upload_dir, exist_ok=True)
backend/app/main.py CHANGED
@@ -173,14 +173,31 @@ def create_app() -> FastAPI:
173
 
174
  return diag_results
175
 
176
- @app.get("/", tags=["Root"])
177
- async def root():
178
- return {
179
- "name": "Enterprise AI Copilot Platform",
180
- "version": "1.0.0",
181
- "docs": "/docs",
182
- "health": "/health",
183
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  return app
186
 
 
173
 
174
  return diag_results
175
 
176
+ # ── HuggingFace Static SPA Serving ────────────────────────────
177
+ import os
178
+ from fastapi.staticfiles import StaticFiles
179
+ from fastapi.responses import FileResponse
180
+
181
+ static_dir = os.path.abspath("/app/static")
182
+ if not os.path.exists(static_dir):
183
+ static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "static"))
184
+
185
+ if os.path.exists(static_dir):
186
+ assets_dir = os.path.join(static_dir, "assets")
187
+ if os.path.exists(assets_dir):
188
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
189
+
190
+ @app.get("/{full_path:path}", include_in_schema=False)
191
+ async def serve_spa(full_path: str):
192
+ if full_path.startswith("api/") or full_path in ["docs", "redoc", "openapi.json", "health"]:
193
+ return JSONResponse(status_code=404, content={"detail": "Not Found"})
194
+ file_path = os.path.join(static_dir, full_path)
195
+ if os.path.exists(file_path) and os.path.isfile(file_path):
196
+ return FileResponse(file_path)
197
+ index_path = os.path.join(static_dir, "index.html")
198
+ if os.path.exists(index_path):
199
+ return FileResponse(index_path)
200
+ return JSONResponse(status_code=404, content={"detail": "Not Found"})
201
 
202
  return app
203
 
backend/app/schemas/auth.py CHANGED
@@ -15,6 +15,7 @@ class RegisterRequest(BaseModel):
15
  password: str
16
  full_name: str
17
  tenant_name: str # Creates a new tenant for this user
 
18
 
19
  @field_validator("password")
20
  @classmethod
 
15
  password: str
16
  full_name: str
17
  tenant_name: str # Creates a new tenant for this user
18
+ role: Optional[UserRole] = None # Optional role override (admin, hr, finance, employee)
19
 
20
  @field_validator("password")
21
  @classmethod
backend/app/services/auth_service.py CHANGED
@@ -64,7 +64,7 @@ async def register_user(
64
  email=data.email,
65
  hashed_password=hash_password(data.password),
66
  full_name=data.full_name,
67
- role=UserRole.ADMIN.value,
68
  tenant_id=tenant.id,
69
  is_active=True,
70
  is_verified=True,
 
64
  email=data.email,
65
  hashed_password=hash_password(data.password),
66
  full_name=data.full_name,
67
+ role=data.role.value if data.role else UserRole.ADMIN.value,
68
  tenant_id=tenant.id,
69
  is_active=True,
70
  is_verified=True,
frontend/src/components/layout/AppLayout.tsx CHANGED
@@ -9,9 +9,9 @@ export default function AppLayout() {
9
  <Sidebar />
10
 
11
  {/* Main content */}
12
- <div className="flex flex-col flex-1 min-w-0">
13
  <Header />
14
- <main className="flex-1 overflow-hidden">
15
  <Outlet />
16
  </main>
17
  </div>
 
9
  <Sidebar />
10
 
11
  {/* Main content */}
12
+ <div className="flex flex-col flex-1 min-w-0 h-full overflow-hidden">
13
  <Header />
14
+ <main className="flex-1 overflow-y-auto min-h-0">
15
  <Outlet />
16
  </main>
17
  </div>
frontend/src/pages/AdminPage.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { useState, useEffect } from 'react';
2
- import { Shield, Users, FileText, UserPlus, RefreshCw, Lock, CheckCircle, XCircle } from 'lucide-react';
3
  import { adminApi } from '@/services/api';
4
  import { clsx } from 'clsx';
5
  import toast from 'react-hot-toast';
@@ -15,7 +15,7 @@ interface UserRecord {
15
 
16
  interface AuditRecord {
17
  id: string;
18
- user_id: str;
19
  action: string;
20
  agent_name?: string;
21
  outcome: string;
@@ -33,10 +33,12 @@ export default function AdminPage() {
33
 
34
  // Invite modal state
35
  const [showInviteModal, setShowInviteModal] = useState(false);
 
36
  const [inviteForm, setInviteForm] = useState({
37
  email: '',
38
  full_name: '',
39
  role: 'employee',
 
40
  });
41
 
42
  const loadAdminData = async () => {
@@ -67,15 +69,29 @@ export default function AdminPage() {
67
  e.preventDefault();
68
  try {
69
  await adminApi.inviteUser(inviteForm);
70
- toast.success(`User ${inviteForm.email} invited!`);
 
 
 
 
71
  setShowInviteModal(false);
72
- setInviteForm({ email: '', full_name: '', role: 'employee' });
73
  loadAdminData();
74
- } catch {
75
- toast.error('Failed to invite user.');
 
 
 
 
76
  }
77
  };
78
 
 
 
 
 
 
 
 
79
  return (
80
  <div className="p-6 lg:p-8 space-y-6 max-w-6xl mx-auto animate-fade-in">
81
  {/* Header */}
@@ -128,7 +144,10 @@ export default function AdminPage() {
128
  <div className="flex justify-between items-center">
129
  <h3 className="text-white font-medium text-sm">Active Workspace Users ({users.length})</h3>
130
  <button
131
- onClick={() => setShowInviteModal(true)}
 
 
 
132
  className="flex items-center gap-2 px-3 py-2 rounded-lg bg-primary-600 hover:bg-primary-500 text-white text-xs font-semibold"
133
  >
134
  <UserPlus size={14} />
@@ -153,7 +172,13 @@ export default function AdminPage() {
153
  <div className="text-xs text-dark-400">{u.email}</div>
154
  </td>
155
  <td className="p-3">
156
- <span className="text-xs px-2 py-0.5 rounded bg-dark-700 text-dark-300 font-semibold uppercase">
 
 
 
 
 
 
157
  {u.role}
158
  </span>
159
  </td>
@@ -237,7 +262,10 @@ export default function AdminPage() {
237
  {showInviteModal && (
238
  <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
239
  <form onSubmit={handleInvite} className="glass-card w-full max-w-md p-6 space-y-4">
240
- <h3 className="text-white font-bold text-lg">Invite New User</h3>
 
 
 
241
  <div>
242
  <label className="text-xs text-dark-300">Full Name</label>
243
  <input
@@ -245,6 +273,7 @@ export default function AdminPage() {
245
  required
246
  value={inviteForm.full_name}
247
  onChange={(e) => setInviteForm({ ...inviteForm, full_name: e.target.value })}
 
248
  className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded text-sm text-white mt-1"
249
  />
250
  </div>
@@ -255,27 +284,42 @@ export default function AdminPage() {
255
  required
256
  value={inviteForm.email}
257
  onChange={(e) => setInviteForm({ ...inviteForm, email: e.target.value })}
 
258
  className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded text-sm text-white mt-1"
259
  />
260
  </div>
261
  <div>
262
- <label className="text-xs text-dark-300">Role</label>
263
  <select
264
  value={inviteForm.role}
265
  onChange={(e) => setInviteForm({ ...inviteForm, role: e.target.value })}
266
  className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded text-sm text-white mt-1"
267
  >
268
- <option value="employee">Employee</option>
269
- <option value="hr">HR</option>
270
- <option value="finance">Finance</option>
271
- <option value="admin">Admin</option>
272
  </select>
273
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  <div className="flex justify-end gap-3 mt-4">
275
  <button
276
  type="button"
277
  onClick={() => setShowInviteModal(false)}
278
- className="px-4 py-2 rounded bg-dark-700 text-dark-300 text-xs font-semibold"
279
  >
280
  Cancel
281
  </button>
@@ -283,12 +327,49 @@ export default function AdminPage() {
283
  type="submit"
284
  className="px-4 py-2 rounded bg-primary-600 hover:bg-primary-500 text-white text-xs font-semibold"
285
  >
286
- Send Invite
287
  </button>
288
  </div>
289
  </form>
290
  </div>
291
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  </div>
293
  );
294
  }
 
1
  import { useState, useEffect } from 'react';
2
+ import { Shield, Users, FileText, UserPlus, RefreshCw, Lock, CheckCircle, XCircle, Copy, Key } from 'lucide-react';
3
  import { adminApi } from '@/services/api';
4
  import { clsx } from 'clsx';
5
  import toast from 'react-hot-toast';
 
15
 
16
  interface AuditRecord {
17
  id: string;
18
+ user_id: string;
19
  action: string;
20
  agent_name?: string;
21
  outcome: string;
 
33
 
34
  // Invite modal state
35
  const [showInviteModal, setShowInviteModal] = useState(false);
36
+ const [inviteSuccess, setInviteSuccess] = useState<{ email: string; password: string; role: string } | null>(null);
37
  const [inviteForm, setInviteForm] = useState({
38
  email: '',
39
  full_name: '',
40
  role: 'employee',
41
+ password: 'Welcome@123!',
42
  });
43
 
44
  const loadAdminData = async () => {
 
69
  e.preventDefault();
70
  try {
71
  await adminApi.inviteUser(inviteForm);
72
+ setInviteSuccess({
73
+ email: inviteForm.email,
74
+ password: inviteForm.password,
75
+ role: inviteForm.role,
76
+ });
77
  setShowInviteModal(false);
 
78
  loadAdminData();
79
+ } catch (err: any) {
80
+ let msg = 'Failed to invite user.';
81
+ if (err?.response?.data?.detail?.message) {
82
+ msg = err.response.data.detail.message;
83
+ }
84
+ toast.error(msg);
85
  }
86
  };
87
 
88
+ const copyCredentials = () => {
89
+ if (!inviteSuccess) return;
90
+ const text = `Login Credentials for Enterprise AI Copilot:\nURL: http://localhost:3000/login\nEmail: ${inviteSuccess.email}\nPassword: ${inviteSuccess.password}\nRole: ${inviteSuccess.role}`;
91
+ navigator.clipboard.writeText(text);
92
+ toast.success('Credentials copied to clipboard!');
93
+ };
94
+
95
  return (
96
  <div className="p-6 lg:p-8 space-y-6 max-w-6xl mx-auto animate-fade-in">
97
  {/* Header */}
 
144
  <div className="flex justify-between items-center">
145
  <h3 className="text-white font-medium text-sm">Active Workspace Users ({users.length})</h3>
146
  <button
147
+ onClick={() => {
148
+ setInviteForm({ email: '', full_name: '', role: 'employee', password: 'Welcome@123!' });
149
+ setShowInviteModal(true);
150
+ }}
151
  className="flex items-center gap-2 px-3 py-2 rounded-lg bg-primary-600 hover:bg-primary-500 text-white text-xs font-semibold"
152
  >
153
  <UserPlus size={14} />
 
172
  <div className="text-xs text-dark-400">{u.email}</div>
173
  </td>
174
  <td className="p-3">
175
+ <span className={clsx(
176
+ 'text-xs px-2 py-0.5 rounded font-semibold uppercase',
177
+ u.role === 'admin' && 'bg-primary-500/20 text-primary-300 border border-primary-500/30',
178
+ u.role === 'hr' && 'bg-pink-500/20 text-pink-300 border border-pink-500/30',
179
+ u.role === 'finance' && 'bg-amber-500/20 text-amber-300 border border-amber-500/30',
180
+ u.role === 'employee' && 'bg-dark-700 text-dark-300'
181
+ )}>
182
  {u.role}
183
  </span>
184
  </td>
 
262
  {showInviteModal && (
263
  <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
264
  <form onSubmit={handleInvite} className="glass-card w-full max-w-md p-6 space-y-4">
265
+ <h3 className="text-white font-bold text-lg flex items-center gap-2">
266
+ <UserPlus size={18} className="text-primary-400" />
267
+ Invite New Team Member
268
+ </h3>
269
  <div>
270
  <label className="text-xs text-dark-300">Full Name</label>
271
  <input
 
273
  required
274
  value={inviteForm.full_name}
275
  onChange={(e) => setInviteForm({ ...inviteForm, full_name: e.target.value })}
276
+ placeholder="Jane Doe"
277
  className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded text-sm text-white mt-1"
278
  />
279
  </div>
 
284
  required
285
  value={inviteForm.email}
286
  onChange={(e) => setInviteForm({ ...inviteForm, email: e.target.value })}
287
+ placeholder="jane@company.com"
288
  className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded text-sm text-white mt-1"
289
  />
290
  </div>
291
  <div>
292
+ <label className="text-xs text-dark-300">Assigned Role</label>
293
  <select
294
  value={inviteForm.role}
295
  onChange={(e) => setInviteForm({ ...inviteForm, role: e.target.value })}
296
  className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded text-sm text-white mt-1"
297
  >
298
+ <option value="employee">πŸ‘€ Employee (General Knowledge)</option>
299
+ <option value="hr">πŸ‘” HR Specialist (Policies & Handbooks)</option>
300
+ <option value="finance">πŸ’° Finance Specialist (Financials & Sales)</option>
301
+ <option value="admin">πŸ‘‘ Admin (Full Workspace Control)</option>
302
  </select>
303
  </div>
304
+ <div>
305
+ <label className="text-xs text-dark-300 flex items-center justify-between">
306
+ <span>Temporary Password</span>
307
+ <span className="text-dark-500 font-normal text-xs">(Provide to employee to log in)</span>
308
+ </label>
309
+ <input
310
+ type="text"
311
+ required
312
+ minLength={8}
313
+ value={inviteForm.password}
314
+ onChange={(e) => setInviteForm({ ...inviteForm, password: e.target.value })}
315
+ className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded text-sm text-white mt-1 font-mono"
316
+ />
317
+ </div>
318
  <div className="flex justify-end gap-3 mt-4">
319
  <button
320
  type="button"
321
  onClick={() => setShowInviteModal(false)}
322
+ className="px-4 py-2 rounded bg-dark-700 text-dark-300 text-xs font-semibold hover:bg-dark-600"
323
  >
324
  Cancel
325
  </button>
 
327
  type="submit"
328
  className="px-4 py-2 rounded bg-primary-600 hover:bg-primary-500 text-white text-xs font-semibold"
329
  >
330
+ Create Account & Invite
331
  </button>
332
  </div>
333
  </form>
334
  </div>
335
  )}
336
+
337
+ {/* Invite Success Confirmation Modal */}
338
+ {inviteSuccess && (
339
+ <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
340
+ <div className="glass-card w-full max-w-md p-6 space-y-4 text-center">
341
+ <div className="w-12 h-12 rounded-full bg-green-500/20 border border-green-500/30 flex items-center justify-center mx-auto text-green-400">
342
+ <CheckCircle size={24} />
343
+ </div>
344
+ <h3 className="text-white font-bold text-lg">User Account Created!</h3>
345
+ <p className="text-dark-300 text-sm">
346
+ The employee account has been added to your workspace. Provide them with these credentials to log in:
347
+ </p>
348
+
349
+ <div className="bg-dark-800 p-4 rounded-lg border border-dark-600 text-left space-y-2 text-sm font-mono">
350
+ <div><span className="text-dark-400">Email:</span> <span className="text-white">{inviteSuccess.email}</span></div>
351
+ <div><span className="text-dark-400">Password:</span> <span className="text-primary-300">{inviteSuccess.password}</span></div>
352
+ <div><span className="text-dark-400">Role:</span> <span className="text-white capitalize">{inviteSuccess.role}</span></div>
353
+ <div><span className="text-dark-400">Login URL:</span> <span className="text-dark-300">http://localhost:3000/login</span></div>
354
+ </div>
355
+
356
+ <div className="flex gap-3 mt-4">
357
+ <button
358
+ onClick={copyCredentials}
359
+ className="flex-1 flex items-center justify-center gap-2 py-2 rounded bg-dark-700 hover:bg-dark-600 text-white text-xs font-semibold"
360
+ >
361
+ <Copy size={14} /> Copy Details
362
+ </button>
363
+ <button
364
+ onClick={() => setInviteSuccess(null)}
365
+ className="flex-1 py-2 rounded bg-primary-600 hover:bg-primary-500 text-white text-xs font-semibold"
366
+ >
367
+ Done
368
+ </button>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ )}
373
  </div>
374
  );
375
  }
frontend/src/pages/DocumentsPage.tsx CHANGED
@@ -5,6 +5,7 @@ import {
5
  Clock, RefreshCw, ChevronDown, AlertCircle, FileUp, MessageSquare,
6
  } from 'lucide-react';
7
  import { documentsApi } from '@/services/api';
 
8
  import { clsx } from 'clsx';
9
  import toast from 'react-hot-toast';
10
 
@@ -70,6 +71,30 @@ function UploadZone({
70
  e.target.value = '';
71
  };
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  return (
74
  <div className="space-y-4">
75
  {/* Doc type selector */}
@@ -84,14 +109,17 @@ function UploadZone({
84
  }}
85
  className="appearance-none pl-3 pr-8 py-1.5 rounded-lg bg-dark-800 border border-dark-600 text-sm text-white focus:outline-none focus:ring-2 focus:ring-primary-500/30 cursor-pointer"
86
  >
87
- <option value="general">General</option>
88
- <option value="hr">HR / Policy</option>
89
- <option value="finance">Finance</option>
90
- <option value="legal">Legal</option>
91
- <option value="technical">Technical</option>
92
  </select>
93
  <ChevronDown size={14} className="absolute right-2 top-1/2 -translate-y-1/2 text-dark-400 pointer-events-none" />
94
  </div>
 
 
 
 
 
95
  </div>
96
 
97
  {/* Drop zone */}
 
5
  Clock, RefreshCw, ChevronDown, AlertCircle, FileUp, MessageSquare,
6
  } from 'lucide-react';
7
  import { documentsApi } from '@/services/api';
8
+ import { useAuthStore } from '@/store/authStore';
9
  import { clsx } from 'clsx';
10
  import toast from 'react-hot-toast';
11
 
 
71
  e.target.value = '';
72
  };
73
 
74
+ const userRole = useAuthStore.getState().user?.role || 'employee';
75
+
76
+ const categoryOptions = (() => {
77
+ if (userRole === 'admin') return [
78
+ { value: 'general', label: 'General' },
79
+ { value: 'hr', label: 'HR / Policy' },
80
+ { value: 'finance', label: 'Finance' },
81
+ { value: 'legal', label: 'Legal' },
82
+ { value: 'technical', label: 'Technical' },
83
+ ];
84
+ if (userRole === 'hr') return [
85
+ { value: 'general', label: 'General' },
86
+ { value: 'hr', label: 'HR / Policy' },
87
+ ];
88
+ if (userRole === 'finance') return [
89
+ { value: 'general', label: 'General' },
90
+ { value: 'finance', label: 'Finance' },
91
+ ];
92
+ return [
93
+ { value: 'general', label: 'General' },
94
+ { value: 'technical', label: 'Technical' },
95
+ ];
96
+ })();
97
+
98
  return (
99
  <div className="space-y-4">
100
  {/* Doc type selector */}
 
109
  }}
110
  className="appearance-none pl-3 pr-8 py-1.5 rounded-lg bg-dark-800 border border-dark-600 text-sm text-white focus:outline-none focus:ring-2 focus:ring-primary-500/30 cursor-pointer"
111
  >
112
+ {categoryOptions.map((opt) => (
113
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
114
+ ))}
 
 
115
  </select>
116
  <ChevronDown size={14} className="absolute right-2 top-1/2 -translate-y-1/2 text-dark-400 pointer-events-none" />
117
  </div>
118
+ {userRole === 'employee' && (
119
+ <span className="text-xs text-amber-400 bg-amber-400/10 border border-amber-400/20 px-2 py-0.5 rounded">
120
+ πŸ”’ Employee: Restricted to General/Tech docs (Admin approval required for HR/Finance)
121
+ </span>
122
+ )}
123
  </div>
124
 
125
  {/* Drop zone */}
frontend/src/pages/RegisterPage.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { useState, FormEvent } from 'react';
2
  import { Link } from 'react-router-dom';
3
- import { Bot, Eye, EyeOff } from 'lucide-react';
4
  import { useAuthStore } from '@/store/authStore';
5
  import toast from 'react-hot-toast';
6
 
@@ -12,9 +12,10 @@ export default function RegisterPage() {
12
  email: '',
13
  password: '',
14
  tenant_name: '',
 
15
  });
16
 
17
- const update = (key: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement>) => {
18
  clearError();
19
  setForm((prev) => ({ ...prev, [key]: e.target.value }));
20
  };
@@ -22,7 +23,7 @@ export default function RegisterPage() {
22
  const handleSubmit = async (e: FormEvent) => {
23
  e.preventDefault();
24
  try {
25
- await register(form);
26
  toast.success('Account created! Welcome aboard πŸŽ‰');
27
  } catch {
28
  // error shown in form
@@ -99,6 +100,27 @@ export default function RegisterPage() {
99
  />
100
  </div>
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  {/* Password */}
103
  <div className="space-y-1.5">
104
  <label className="text-sm font-medium text-dark-200" htmlFor="reg-password">Password</label>
@@ -137,13 +159,9 @@ export default function RegisterPage() {
137
  Creating account...
138
  </span>
139
  ) : (
140
- 'Create free account'
141
  )}
142
  </button>
143
-
144
- <p className="text-xs text-dark-500 text-center">
145
- You'll be the Admin of your company workspace.
146
- </p>
147
  </form>
148
  </div>
149
  </div>
 
1
  import { useState, FormEvent } from 'react';
2
  import { Link } from 'react-router-dom';
3
+ import { Bot, Eye, EyeOff, ChevronDown } from 'lucide-react';
4
  import { useAuthStore } from '@/store/authStore';
5
  import toast from 'react-hot-toast';
6
 
 
12
  email: '',
13
  password: '',
14
  tenant_name: '',
15
+ role: 'admin',
16
  });
17
 
18
+ const update = (key: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
19
  clearError();
20
  setForm((prev) => ({ ...prev, [key]: e.target.value }));
21
  };
 
23
  const handleSubmit = async (e: FormEvent) => {
24
  e.preventDefault();
25
  try {
26
+ await register(form as any);
27
  toast.success('Account created! Welcome aboard πŸŽ‰');
28
  } catch {
29
  // error shown in form
 
100
  />
101
  </div>
102
 
103
+ {/* User Role Selection */}
104
+ <div className="space-y-1.5">
105
+ <label className="text-sm font-medium text-dark-200" htmlFor="role">
106
+ Account Role
107
+ </label>
108
+ <div className="relative">
109
+ <select
110
+ id="role"
111
+ value={form.role}
112
+ onChange={update('role')}
113
+ className="input-glow w-full px-4 py-2.5 rounded-lg bg-dark-800 border border-dark-600 text-white text-sm appearance-none cursor-pointer"
114
+ >
115
+ <option value="admin">πŸ‘‘ Admin (Full Access)</option>
116
+ <option value="hr">πŸ‘” HR Specialist (Policies & Handbooks)</option>
117
+ <option value="finance">πŸ’° Finance Specialist (Financials & Sales)</option>
118
+ <option value="employee">πŸ‘€ Standard Employee (General Knowledge)</option>
119
+ </select>
120
+ <ChevronDown size={16} className="absolute right-3 top-1/2 -translate-y-1/2 text-dark-400 pointer-events-none" />
121
+ </div>
122
+ </div>
123
+
124
  {/* Password */}
125
  <div className="space-y-1.5">
126
  <label className="text-sm font-medium text-dark-200" htmlFor="reg-password">Password</label>
 
159
  Creating account...
160
  </span>
161
  ) : (
162
+ 'Create account'
163
  )}
164
  </button>
 
 
 
 
165
  </form>
166
  </div>
167
  </div>