Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
5011260
chore: add uploads folder with .gitkeep to ensure folder structure is…
osctoss May 29, 2026
2581a0c
Latest FAQ updates
abhi03241 May 29, 2026
cab03a9
Merge branch 'main' of https://github.com/vicharanashala/cs20.git int…
osctoss May 29, 2026
d78da25
feat(chunks 7-10): global search, RTQ category filter, QP wire-up, ac…
abhi03241 May 29, 2026
e70f4ee
Merge branch 'main' of https://github.com/vicharanashala/cs20.git int…
osctoss May 29, 2026
f9ce1db
fix: disable transformer embedder (USE_TRANSFORMER=false) to avoid im…
abhi03241 May 29, 2026
a6ef685
fix: disable transformer embedder, add admin controller methods, wire…
abhi03241 May 30, 2026
1156202
Merge branch 'main' of https://github.com/vicharanashala/cs20 into FAQ-m
osctoss May 30, 2026
8974953
fix: FAQ categories display and API bugs - sync client/server categor…
abhi03241 May 30, 2026
5151212
chore: add test script for aggregation debugging
abhi03241 May 30, 2026
445fa2c
feat: integrate Sentence Transformer + Qdrant Cloud ANN duplicate det…
osctoss May 30, 2026
282cc14
Merge remote branch 'origin/teammate' into FAQ-m
osctoss May 30, 2026
3630ed1
Merge pull request #1 from osctoss/teammate
abhi03241 May 30, 2026
e692765
Merge pull request #2 from vicharanashala/teammate
abhi03241 May 30, 2026
263f7a5
Hide Ask a Question button for Admin/Senior and remove Notifications …
osctoss May 30, 2026
0c36300
Update CONTEXT.md with role-based UI constraints and dashboard refine…
osctoss May 30, 2026
30c5cea
Merge pull request #3 from osctoss/main
abhi03241 May 30, 2026
3b3f4c0
Standardize FAQ_CATEGORIES to the 10 requested clean categories and a…
osctoss May 30, 2026
d918b88
Update CONTEXT.md to document standardized FAQ/RTQ categories and mig…
osctoss May 30, 2026
9da8d82
Fix All Categories filter on FAQPage showing only General category
osctoss May 30, 2026
49196e1
Update CONTEXT.md with FAQPage All Categories filter fix
osctoss May 30, 2026
f4ba6bc
Fix category upvotes for non-General categories by normalizing groupe…
osctoss May 30, 2026
681e8da
Implement default category sorting by upvotes and fix item sorting fi…
osctoss May 30, 2026
bc9d592
Fix category upvoting, category filtering, and toggleable upvotes on …
osctoss May 30, 2026
d551677
Merge pull request #4 from osctoss/main
abhi03241 Jun 1, 2026
ef84a51
feat: implement RTQ (Real-Time Question) system including core API co…
osctoss Jun 2, 2026
4786cc7
Merge branch 'main' of https://github.com/vicharanashala/cs20
osctoss Jun 2, 2026
44b5c68
feat: show user role status badge on dashboards
osctoss Jun 2, 2026
fb0f1aa
docs: document dashboard role status badges in CONTEXT.md
osctoss Jun 2, 2026
f3a5cb8
feat: complete moderator feature audit and implement missing capabili…
osctoss Jun 2, 2026
74acc8f
Refactor moderation controls with icons, implement decision toggling …
osctoss Jun 2, 2026
0331558
Merge branch 'abhi03241:main' into FAQ-m
osctoss Jun 2, 2026
0e01bc7
Merge pull request #5 from osctoss/teammate
abhi03241 Jun 2, 2026
3f20ba6
Merge pull request #6 from vicharanashala/teammate
abhi03241 Jun 2, 2026
b28a196
fix: admin answer resolves RTQ; no QP awarded after RTQ is already re…
abhi03241 Jun 2, 2026
07ed89f
Merge branch 'origin/main' and resolve conflicts in rtq.controller.js
osctoss Jun 2, 2026
157a6f6
Merge branch 'FAQ-m' of https://github.com/osctoss/FAQ into FAQ-m
osctoss Jun 2, 2026
f193abb
Merge pull request #7 from osctoss/teammate
abhi03241 Jun 2, 2026
a37adb8
Merge pull request #8 from vicharanashala/teammate
abhi03241 Jun 2, 2026
ee9840c
Implement Senior RTQ moderation features, dynamic QP rules, and prior…
osctoss Jun 2, 2026
f27b46d
Implement controlled FAQ conversion flow with Senior Review Edit Moda…
osctoss Jun 2, 2026
334a98f
Upgrade working history page to filter and list only the questions co…
osctoss Jun 2, 2026
ad06032
Update CONTEXT.md with Controlled Senior FAQ Flow, Bidirectional Trac…
osctoss Jun 2, 2026
302ec8c
Upgrade Notifications page UI with colorful circular category tags, c…
osctoss Jun 2, 2026
37182cf
feat: leaderboard UI, block/unblock users, email change restriction, …
abhi03241 Jun 2, 2026
463d818
Merge pull request #9 from osctoss/teammate
abhi03241 Jun 2, 2026
b35b870
Merge branch 'origin/main' into FAQ-m resolving conflicts, keeping Mo…
osctoss Jun 2, 2026
2dc5d0a
style: fix font import order in index.css to resolve build warning
osctoss Jun 2, 2026
7fdc235
docs: update CONTEXT.md with git merge conflict resolution and css wa…
osctoss Jun 2, 2026
b58b731
fix: align moderator question accept status, prevent auto-resolve and…
osctoss Jun 2, 2026
e76ad05
feat: implement toggle markedForReview for RTQ and FAQ review flags
osctoss Jun 2, 2026
503988e
Merge branch 'vicharanashala:teammate' into teammate
osctoss Jun 2, 2026
7862c8b
feat: implement users list toggling, rename tiers to Peers and Senior…
osctoss Jun 3, 2026
4e55a76
Merge remote-tracking branch 'my-cs20-fork/teammate' into FAQ-m
osctoss Jun 3, 2026
e8e93cc
feat: implement role-based highlighting and track question status dro…
osctoss Jun 3, 2026
a9b3a58
Merge pull request #10 from osctoss/teammate
abhi03241 Jun 3, 2026
492b8f3
Merge pull request #11 from vicharanashala/teammate
abhi03241 Jun 3, 2026
9538cc2
feat: public FAQ mode, QP history page, notification delete, QP syste…
abhi03241 Jun 3, 2026
1477258
added /
abhi03241 Jun 3, 2026
b72b71e
UI/UX enhancements: premium widgets, category scroller, profile layou…
osctoss Jun 3, 2026
f3c131e
UI/UX enhancements: restore LoginModal popup for anonymous upvotes an…
osctoss Jun 3, 2026
38b3edb
feat: integrate brand logo icon & warm amber-bronze theme, and add re…
osctoss Jun 3, 2026
3b3b926
UI/UX enhancements: add About page, logo symbolism, platform features…
osctoss Jun 4, 2026
c1d7f44
UI/UX: fix crash on About page by importing useAuth and defining user
osctoss Jun 4, 2026
9634a63
UI/UX: Refine logo symbolism interactive section symbols and resolve …
osctoss Jun 4, 2026
7e469da
UI/UX: Add directed release animations and fix Q-card text overflow
osctoss Jun 4, 2026
40244d1
docs: Update CONTEXT.md with About Page interactive symbolism details
osctoss Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ CHANGELOG.md
# yarn.lock

# Uploads / media
uploads/
uploads/*
!uploads/.gitkeep

# Test coverage
coverage/
366 changes: 196 additions & 170 deletions CONTEXT.md

Large diffs are not rendered by default.

98 changes: 48 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<<<<<<< HEAD
# Q&A Platform
# PippaQ — Premium Q&A & FAQ Platform

A semantic query-resolution and FAQ generation platform with a **QP (Quality Point) reputation economy** and **role-based access control**. Built with React, Express, MongoDB, and a custom RAG engine.
A high-performance, semantic query-resolution and FAQ generation platform with a **QP (Quality Point) reputation economy**, **role-based access control**, and a production-ready **RAG (Retrieval-Augmented Generation) duplicate detection engine**.

Built with React, Express, MongoDB, Qdrant Cloud, and local Sentence Transformers.

---

## 🏗️ Architecture

```
```text
D:\faq\
├── client/ React + Vite frontend (Tailwind CSS)
├── server/ Express.js backend (MongoDB/Mongoose)
├── rag-engine/ Custom TF-IDF RAG pipeline (no external AI API)
├── client/ React + Vite frontend (Tailwind CSS + Premium Typography)
├── server/ Express.js backend (MongoDB/Mongoose + Qdrant Integration)
├── rag-engine/ Semantic Decision Engine (Sentence Transformer + Qdrant ANN)
├── shared/ Shared constants (QP rules, RAG thresholds, roles)
└── docs/ Project documentation
```
Expand All @@ -30,21 +31,21 @@ npm run install:all

```bash
cp server/.env.example server/.env
# Edit server/.env — set your MongoDB URI and JWT secret
# Edit server/.env — set your MongoDB URI, JWT secret, and QDRANT_URL
```

### 3. Start MongoDB
### 3. Start MongoDB & Qdrant

Make sure MongoDB is running locally or update `MONGO_URI` in `server/.env`.
Ensure your local or cloud MongoDB and Qdrant instances are accessible.

### 4. Run the app

```bash
npm run dev
```

- **Client**: http://localhost:3000
- **Server**: http://localhost:5000
* **Client**: http://localhost:3000
* **Server**: http://localhost:5000

---

Expand Down Expand Up @@ -80,29 +81,41 @@ npm run dev
| Question removed | -5 |

### Thresholds
- **QP < 50** → Cannot raise questions
- **QP ≥ 500** → Auto-request Moderator promotion
* **QP < 50** → Cannot raise questions.
* **QP ≥ 500** → Auto-request Moderator promotion.

---

## 🧠 RAG Decision Tree

Questions are evaluated against the FAQ and RTQ vector stores:

```
User Question → TF-IDF Embed (384-dim) → Compare with FAQ/RTQ vectors

F1: FAQ similarity > 80% → REJECT + -5 QP (duplicate)
F2: 50–80% FAQ similarity
+ R1: RTQ > 60% → REJECT + -5 QP (similar RTQ exists)
+ R2: 20–60% RTQ → REJECT (no penalty)
+ R3: RTQ ≤ 20% → ACCEPT → Add to RTQ
F3: FAQ ≤ 50% similarity
+ R1: RTQ > 60% → REJECT (no penalty)
+ R2/R3: RTQ ≤ 60% → ACCEPT → Add to RTQ
## 🧠 RAG Duplicate Detection Engine

Questions are evaluated semantically against the FAQ and RTQ collections stored in Qdrant:

```text
User Question
Generate Embedding ──► Local Sentence Transformer (all-MiniLM-L6-v2, 384-dim)
Qdrant HNSW ANN Search ──► Compare with FAQ/RTQ Collections
Apply Decision Tree Rules:
┌───────────────────────────────┬───────────────────────────────┐
│ F1: FAQ similarity > 80% │ REJECT, -5 QP, upvote FAQ │
├───────────────────────────────┼───────────────────────────────┤
│ F2: FAQ similarity 50–80% │ │
│ ├── R1: RTQ > 60% │ REJECT, -5 QP, upvote FAQ │
│ ├── R2: RTQ 20–60% │ REJECT (no penalty) │
│ └── R3: RTQ ≤ 20% │ ACCEPT → Route to RTQ │
├───────────────────────────────┼───────────────────────────────┤
│ F3: FAQ similarity ≤ 50% │ │
│ ├── R1: RTQ > 60% │ REJECT (no penalty), upvote RTQ│
│ └── R2/R3: RTQ ≤ 60% │ ACCEPT → Route to RTQ │
└───────────────────────────────┴───────────────────────────────┘
```

> Uses TF-IDF character n-grams (no external AI API required). Swap for OpenAI/sentence-transformers in production.
> **Performance Optimization:** Includes an **LRU Embedding Cache** (500 entries) and model warmup on server startup to ensure lightning-fast semantic queries under 10ms.

---

Expand All @@ -124,23 +137,8 @@ F3: FAQ ≤ 50% similarity

## 🛠️ Tech Stack

- **Frontend**: React 18, Vite, React Router v6, Axios, Tailwind CSS
- **Backend**: Express.js, Mongoose, JWT (bcryptjs for passwords)
- **RAG Engine**: Custom TF-IDF n-gram embedder (384-dim), cosine similarity, in-memory vector DB
- **Database**: MongoDB (vectors stored in documents — swap for Qdrant/Pinecone in production)
- **Dev**: `concurrently` to run client + server together

---

## 📝 Notes

- OTP is **console-logged** in development. Wire to an email provider (SendGrid, Resend) for production.
- Vector DB is **in-memory** (vectors stored in MongoDB documents). For production, swap to Qdrant or Pinecone.
- RESTRICTED users (flagged by seniors) are blocked from asking questions and answering.

---

*Maintained with git. Run `git log --oneline` for full history.*
=======
# FAQ
>>>>>>> 655d58505ffd99f5232d6f5c4fb351452148d89f
* **Frontend**: React 18, Vite, React Router v6, Axios, Tailwind CSS + Premium Typography (Playfair Display & Outfit)
* **Backend**: Express.js, Mongoose, JWT (bcryptjs)
* **Vector Store**: Qdrant Cloud (HNSW indexes, cosine distance, metadata payload filters)
* **Embeddings**: Native Node.js `@xenova/transformers` (local WebAssembly ONNX execution of `all-MiniLM-L6-v2`)
* **Dev Tools**: `concurrently` to run client + server together
Binary file added assets/PippaQ1.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Q&A Platform</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐛</text></svg>" />
<meta name="description" content="PippaQ — A premium semantic Q&A platform with an intelligent FAQ knowledge base, community-driven answers, and a gamified QP reputation economy." />
<meta name="theme-color" content="#4f46e5" />
<title>PippaQ — Premium Q&A Platform</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Outfit:wght@100..900&display=swap" rel="stylesheet">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎓</text></svg>" />
</head>
<body>
<div id="root"></div>
Expand Down
Binary file added client/public/PippaQ1.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 75 additions & 34 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { useState, useEffect } from 'react';
import { useAuth } from './context/AuthContext';
import Nav from './components/Nav';
import ErrorBoundary from './components/ErrorBoundary';
import { ToastProvider } from './components/Toast';
import GlobalSearch from './components/GlobalSearch';

// Pages
import LoginPage from './pages/LoginPage';
import SignupPage from './pages/SignupPage';
import FAQPage from './pages/FAQPage';
import FAQEditPage from './pages/FAQEditPage';
import RTQPage from './pages/RTQPage';
import RTQDetailPage from './pages/RTQDetailPage';
import StudentDashboard from './pages/StudentDashboard';
import SeniorDashboard from './pages/SeniorDashboard';
import AddFAQPage from './pages/AddFAQPage';
import RaiseQuestionPage from './pages/RaiseQuestionPage';
import ProfilePage from './pages/ProfilePage';
import UserListPage from './pages/UserListPage';
import UserProfilePage from './pages/UserProfilePage';
import TrackQuestionPage from './pages/TrackQuestionPage';
import WorkingHistoryPage from './pages/WorkingHistoryPage';
import NotificationsPage from './pages/NotificationsPage';
import QPHistoryPage from './pages/QPHistoryPage';
import AboutPage from './pages/AboutPage';

function LoadingScreen() {
return (
<div className="min-h-screen flex items-center justify-center bg-surface">
<div className="text-muted">Loading...</div>
<div className="min-h-screen flex items-center justify-center bg-mesh">
<div className="flex flex-col items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-accent to-violet-600 flex items-center justify-center animate-pulse">
<span className="text-white font-brand font-bold text-lg">P</span>
</div>
<div className="text-muted text-sm font-medium">Loading...</div>
</div>
</div>
);
}
Expand All @@ -42,7 +55,6 @@ function PublicOnly({ children }) {
return children;
}

// Dashboard renders correct page based on role
function DashboardRoute() {
const { user, loading } = useAuth();
if (loading) return <LoadingScreen />;
Expand All @@ -51,41 +63,70 @@ function DashboardRoute() {
return <StudentDashboard />;
}

const PUBLIC_PATHS = ['/login', '/signup'];

export default function App() {
// Scroll to top on route change
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}

function AppLayout() {
const location = useLocation();
const { user, refreshUser } = useAuth();
const isPublic = PUBLIC_PATHS.includes(location.pathname);
const [searchOpen, setSearchOpen] = useState(false);

useEffect(() => {
const onKey = (e) => {
if (e.key === '/' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
e.preventDefault();
setSearchOpen(true);
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);

return (
<div className="min-h-screen bg-surface">
{user && !isPublic && <Nav refreshUser={refreshUser} />}
<Routes>
{/* Public */}
<Route path="/login" element={<PublicOnly><LoginPage /></PublicOnly>} />
<Route path="/signup" element={<PublicOnly><SignupPage /></PublicOnly>} />

{/* Dashboard — role-aware */}
<Route path="/dashboard" element={<DashboardRoute />} />

{/* Protected — all authenticated users */}
<Route path="/faq" element={<ProtectedRoute><FAQPage /></ProtectedRoute>} />
<Route path="/rtq" element={<ProtectedRoute><RTQPage /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><ProfilePage /></ProtectedRoute>} />
<Route path="/users" element={<ProtectedRoute><UserListPage /></ProtectedRoute>} />
<Route path="/track" element={<ProtectedRoute><TrackQuestionPage /></ProtectedRoute>} />
<Route path="/history" element={<ProtectedRoute><WorkingHistoryPage /></ProtectedRoute>} />
<Route path="/notifications" element={<ProtectedRoute><NotificationsPage /></ProtectedRoute>} />

{/* Role-restricted */}
<Route path="/raise-question" element={<ProtectedRoute allowedRoles={['student', 'moderator']}><RaiseQuestionPage /></ProtectedRoute>} />
<Route path="/add-faq" element={<ProtectedRoute allowedRoles={['senior', 'admin']}><AddFAQPage /></ProtectedRoute>} />

{/* Redirects */}
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
<ScrollToTop />
{user && !['/login', '/signup'].includes(location.pathname) && <Nav refreshUser={refreshUser} />}
{searchOpen && <GlobalSearch onClose={() => setSearchOpen(false)} />}
<div key={location.pathname} className="page-enter">
<Routes>
<Route path="/login" element={<PublicOnly><LoginPage /></PublicOnly>} />
<Route path="/signup" element={<PublicOnly><SignupPage /></PublicOnly>} />
<Route path="/dashboard" element={<DashboardRoute />} />
<Route path="/faq" element={<FAQPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/faq/edit/:id" element={<ProtectedRoute allowedRoles={['senior', 'admin']}><FAQEditPage /></ProtectedRoute>} />
<Route path="/rtq" element={<ProtectedRoute><RTQPage /></ProtectedRoute>} />
<Route path="/rtq/:id" element={<ProtectedRoute><RTQDetailPage /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><ProfilePage /></ProtectedRoute>} />
<Route path="/users" element={<ProtectedRoute><UserListPage /></ProtectedRoute>} />
<Route path="/users/:id" element={<ProtectedRoute><UserProfilePage /></ProtectedRoute>} />
<Route path="/track" element={<ProtectedRoute allowedRoles={['student', 'moderator']}><TrackQuestionPage /></ProtectedRoute>} />
<Route path="/history" element={<ProtectedRoute><WorkingHistoryPage /></ProtectedRoute>} />
<Route path="/notifications" element={<ProtectedRoute><NotificationsPage /></ProtectedRoute>} />
<Route path="/qp-history" element={<ProtectedRoute><QPHistoryPage /></ProtectedRoute>} />
<Route path="/raise-question" element={<ProtectedRoute allowedRoles={['student', 'moderator']}><RaiseQuestionPage /></ProtectedRoute>} />
<Route path="/add-faq" element={<ProtectedRoute allowedRoles={['senior', 'admin']}><AddFAQPage /></ProtectedRoute>} />
<Route path="/" element={<Navigate to="/faq" replace />} />
<Route path="*" element={<Navigate to="/faq" replace />} />
</Routes>
</div>
</div>
);
}
}

export default function App() {
return (
<ErrorBoundary>
<ToastProvider>
<AppLayout />
</ToastProvider>
</ErrorBoundary>
);
}
7 changes: 5 additions & 2 deletions client/src/components/AnswerCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ export default function AnswerCard({ answer, onUpvote, showModeratorControls = f
<p className="text-xs text-muted">
Answered by <span className="font-medium text-primary">{answer.userId.name}</span>
{answer.userId?.role === 'moderator' && (
<span className="ml-1 text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">Moderator</span>
<span className="ml-1 text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">Moderator</span>
)}
{answer.userId?.role === 'senior' && (
<span className="ml-1 text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">Senior</span>
<span className="ml-1 text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">Senior</span>
)}
{answer.userId?.role === 'admin' && (
<span className="ml-1 text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">Admin</span>
)}
</p>
)}
Expand Down
43 changes: 43 additions & 0 deletions client/src/components/Avatar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useMemo } from 'react';

const ROLE_COLORS = {
student: 'bg-slate-500',
moderator: 'bg-blue-600',
senior: 'bg-purple-600',
admin: 'bg-violet-600',
};

const ROLE_GRADIENTS = {
student: 'from-slate-400 to-slate-600',
moderator: 'from-blue-500 to-blue-700',
senior: 'from-purple-500 to-violet-600',
admin: 'from-violet-500 to-purple-700',
};

const SIZE_CLASSES = {
xs: 'avatar-xs',
sm: 'avatar-sm',
md: 'avatar-md',
lg: 'avatar-lg',
xl: 'avatar-xl',
};

export default function Avatar({ name, role, size = 'md', className = '', gradient = false }) {
const initial = useMemo(() => {
if (!name) return '?';
return name.charAt(0).toUpperCase();
}, [name]);

const bgClass = gradient
? `bg-gradient-to-br ${ROLE_GRADIENTS[role] || ROLE_GRADIENTS.student}`
: ROLE_COLORS[role] || ROLE_COLORS.student;

return (
<div
className={`${SIZE_CLASSES[size] || SIZE_CLASSES.md} ${bgClass} ${className}`}
title={name || 'User'}
>
{initial}
</div>
);
}
Loading