From 2a03a2992f75db80e3e30a084e22f8490bac2368 Mon Sep 17 00:00:00 2001 From: Shashwat Singh <72199492+Edictor96@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:42:21 +0530 Subject: [PATCH 1/9] persist and surface recent searches across the app --- client/src/api/searchApi.js | 10 ++ client/src/components/RecentSearchesPanel.jsx | 73 ++++++++ client/src/components/SearchBar.jsx | 160 ++++++++++++++---- client/src/pages/AskQuestion.jsx | 20 ++- client/src/pages/DashboardHome.jsx | 10 ++ client/src/pages/QueryPage.jsx | 9 + client/src/styles/search.css | 37 ++++ server/controllers/searchController.js | 34 ++++ server/models/User.js | 5 + server/routes/searchRoutes.js | 4 + 10 files changed, 325 insertions(+), 37 deletions(-) create mode 100644 client/src/components/RecentSearchesPanel.jsx diff --git a/client/src/api/searchApi.js b/client/src/api/searchApi.js index fff9994..8055eca 100644 --- a/client/src/api/searchApi.js +++ b/client/src/api/searchApi.js @@ -27,4 +27,14 @@ export async function askFAQ(query, history = []) { export async function sendFeedback(faqId, helpful) { const { data } = await api.post('/faqs/feedback', { faqId, helpful }); return data; +} + +export async function getRecentSearches() { + const { data } = await api.get('/search/recent'); + return data; +} + +export async function saveRecentSearch(query) { + const { data } = await api.post('/search/recent', { query }); + return data; } \ No newline at end of file diff --git a/client/src/components/RecentSearchesPanel.jsx b/client/src/components/RecentSearchesPanel.jsx new file mode 100644 index 0000000..ff6962e --- /dev/null +++ b/client/src/components/RecentSearchesPanel.jsx @@ -0,0 +1,73 @@ +import { useEffect, useState } from 'react'; +import { getRecentSearches } from '../api/searchApi'; + +const LOCAL_KEY = 'recentSearches_v1'; + +export default function RecentSearchesPanel({ title = 'Recent searches', onSelect, className = '' }) { + const [recentSearches, setRecentSearches] = useState([]); + + useEffect(() => { + let mounted = true; + + const loadRecentSearches = async () => { + let merged = []; + + try { + const raw = localStorage.getItem(LOCAL_KEY); + if (raw) { + const localItems = JSON.parse(raw); + if (Array.isArray(localItems)) merged = localItems; + } + } catch (_) { + // ignore localStorage issues + } + + try { + const data = await getRecentSearches(); + if (data?.success && Array.isArray(data.searches)) { + merged = [...new Set([...(data.searches || []), ...merged])].slice(0, 10); + } + } catch (_) { + // ignore unauthenticated/network errors and keep local fallback + } + + if (mounted) { + setRecentSearches(merged); + } + }; + + loadRecentSearches(); + return () => { + mounted = false; + }; + }, []); + + if (!recentSearches.length) return null; + + return ( +
+

{title}

+
+ {recentSearches.map((item) => ( + + ))} +
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/SearchBar.jsx b/client/src/components/SearchBar.jsx index d9ba6cb..dfe08b5 100644 --- a/client/src/components/SearchBar.jsx +++ b/client/src/components/SearchBar.jsx @@ -1,16 +1,66 @@ import { useState, useRef, useEffect, useCallback } from 'react'; +import { getRecentSearches as apiGetRecent, saveRecentSearch as apiSaveRecent } from '../api/searchApi'; + +const RECENT_KEY = 'recentSearches_v1'; export default function SearchBar({ onSearch, onSelectSuggestion, loading, onQueryChange, onKeyDown }) { const [query, setQuery] = useState(''); const [focused, setFocused] = useState(false); + const [recent, setRecent] = useState([]); const inputRef = useRef(null); + useEffect(() => { + try { + const raw = localStorage.getItem(RECENT_KEY); + if (raw) setRecent(JSON.parse(raw)); + } catch (e) { + // ignore + } + }, []); + + useEffect(() => { + // try fetching server-side recent searches (if user authenticated) + let mounted = true; + (async () => { + try { + const res = await apiGetRecent(); + if (mounted && res?.success && Array.isArray(res.searches) && res.searches.length > 0) { + setRecent(res.searches); + localStorage.setItem(RECENT_KEY, JSON.stringify(res.searches)); + } + } catch (e) { + // ignore if unauthenticated or network error + } + })(); + return () => { mounted = false; }; + }, []); + + const saveRecent = useCallback((q) => { + try { + if (!q) return; + const normalized = q.trim(); + if (!normalized) return; + const next = [normalized, ...recent.filter((r) => r !== normalized)].slice(0, 10); + setRecent(next); + localStorage.setItem(RECENT_KEY, JSON.stringify(next)); + // attempt to persist server-side; ignore failures + (async () => { + try { + await apiSaveRecent(normalized); + } catch (_) {} + })(); + } catch (e) { + // ignore + } + }, [recent]); + const handleSubmit = useCallback((e) => { e?.preventDefault(); if (query.trim().length >= 3) { onSearch(query.trim()); + saveRecent(query.trim()); } - }, [query, onSearch]); + }, [query, onSearch, saveRecent]); const handleKeyDown = (e) => { if (e.key === 'Enter') { @@ -19,43 +69,81 @@ export default function SearchBar({ onSearch, onSelectSuggestion, loading, onQue onKeyDown?.(e); }; + useEffect(() => { + const handler = (e) => { + // focus search when user presses `/` and focus isn't on an input or textarea + if (e.key === '/' && document.activeElement && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) { + e.preventDefault(); + inputRef.current?.focus(); + } + if (e.key === 'Escape') { + // clear and blur + setQuery(''); + inputRef.current?.blur(); + onSelectSuggestion?.(null); + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [onSelectSuggestion]); + + const handleSelectRecent = (q) => { + setQuery(q); + onQueryChange?.(q); + onSearch?.(q); + saveRecent(q); + }; + return ( -
-
- - - - - { setQuery(e.target.value); onQueryChange?.(e.target.value); }} - onFocus={() => setFocused(true)} - onBlur={() => setTimeout(() => setFocused(false), 200)} - onKeyDown={handleKeyDown} - minLength={3} - maxLength={300} - autoComplete="off" - /> - {loading && ( - - )} - {!loading && query && ( - + )} + - )} - -
-
+ + + + {focused && !query && recent && recent.length > 0 && ( +
+
Recent searches
+ {recent.map((r, i) => ( + + ))} +
+ )} + ); } diff --git a/client/src/pages/AskQuestion.jsx b/client/src/pages/AskQuestion.jsx index 948ba59..f6ddd3f 100644 --- a/client/src/pages/AskQuestion.jsx +++ b/client/src/pages/AskQuestion.jsx @@ -1,7 +1,8 @@ import { useState, useEffect, useRef } from 'react'; import questionService from '../services/questionService'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import api from '../api/axios'; +import RecentSearchesPanel from '../components/RecentSearchesPanel'; const DRAFT_KEY = 'ask_question_draft'; @@ -14,6 +15,7 @@ const AskQuestion = () => { const [duplicates, setDuplicates] = useState([]); const [dupLoading, setDupLoading] = useState(false); const navigate = useNavigate(); + const location = useLocation(); const debounceRef = useRef(null); useEffect(() => { @@ -25,6 +27,14 @@ const AskQuestion = () => { } }, []); + useEffect(() => { + const prefillTitle = location.state?.prefillTitle; + if (prefillTitle && !title) { + setTitle(prefillTitle); + checkDuplicates(prefillTitle); + } + }, [location.state]); + useEffect(() => { if (!title && !description) return; const timer = setTimeout(() => { @@ -74,6 +84,11 @@ const AskQuestion = () => { setDuplicates([]); }; + const handleRecentSelect = (value) => { + setTitle(value); + checkDuplicates(value); + }; + return (

Ask a Question

@@ -144,6 +159,9 @@ const AskQuestion = () => {
+
+ +
); }; diff --git a/client/src/pages/DashboardHome.jsx b/client/src/pages/DashboardHome.jsx index 6dcd3d1..00396e1 100644 --- a/client/src/pages/DashboardHome.jsx +++ b/client/src/pages/DashboardHome.jsx @@ -1,9 +1,12 @@ import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import questionService from '../services/questionService'; +import RecentSearchesPanel from '../components/RecentSearchesPanel'; const DashboardHome = () => { const [stats, setStats] = useState({ asked: 0, answered: 0, pending: 0 }); const [loading, setLoading] = useState(true); + const navigate = useNavigate(); useEffect(() => { const fetchQuestions = async () => { @@ -46,6 +49,13 @@ const DashboardHome = () => { )} + +
+ navigate('/ask-question', { state: { prefillTitle: value } })} + /> +
); }; diff --git a/client/src/pages/QueryPage.jsx b/client/src/pages/QueryPage.jsx index 382b5d2..60cdd79 100644 --- a/client/src/pages/QueryPage.jsx +++ b/client/src/pages/QueryPage.jsx @@ -3,6 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom'; import toast from 'react-hot-toast'; import api from '../api/axios'; import { useAuth } from '../context/AuthContext'; +import RecentSearchesPanel from '../components/RecentSearchesPanel'; export default function QueryPage() { const { user, logout } = useAuth(); @@ -65,6 +66,10 @@ export default function QueryPage() { toast.success('Logged out'); }; + const handleRecentSelect = (value) => { + setForm((prev) => ({ ...prev, question: value })); + }; + return (
@@ -145,6 +150,10 @@ export default function QueryPage() { {loading ? 'Submitting...' : 'Submit Query'} + +
+ +
diff --git a/client/src/styles/search.css b/client/src/styles/search.css index a98ac6a..e2a15c6 100644 --- a/client/src/styles/search.css +++ b/client/src/styles/search.css @@ -494,6 +494,43 @@ color: var(--accent); } +/* lightweight recent-search dropdown used by SearchBar */ +.search-recent { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + padding: 8px; + z-index: 55; +} + +.search-recent-header { + font-size: 12px; + color: var(--text-muted); + margin: 6px 8px; +} + +.search-recent-item { + display: block; + width: 100%; + padding: 8px 12px; + border: none; + background: transparent; + text-align: left; + color: var(--text-primary); + font-family: var(--font); + cursor: pointer; + border-radius: 8px; +} + +.search-recent-item:hover { + background: var(--accent-light); +} + /* ===== Error ===== */ .faq-page-error { margin-top: 20px; diff --git a/server/controllers/searchController.js b/server/controllers/searchController.js index 3402e37..2ecebea 100644 --- a/server/controllers/searchController.js +++ b/server/controllers/searchController.js @@ -101,4 +101,38 @@ exports.suggestions = async (req, res, next) => { } catch (err) { next(err); } +}; + +// Get recent searches for authenticated user +exports.getRecentSearches = async (req, res, next) => { + try { + if (!req.user) return res.json({ success: true, searches: [] }); + const user = await req.user.populate('recentSearches'); // noop but ensures fresh + const searches = (user.recentSearches || []).slice(0, 10); + res.json({ success: true, searches }); + } catch (err) { + next(err); + } +}; + +// Save a recent search for authenticated user +exports.saveRecentSearch = async (req, res, next) => { + try { + const { query } = req.body; + if (!req.user) return res.status(401).json({ success: false, message: 'Authentication required' }); + if (!query || typeof query !== 'string') { + return next(new AppError('Query is required', 400)); + } + const trimmed = query.trim(); + if (!trimmed) return res.json({ success: true, searches: req.user.recentSearches || [] }); + + // update user's recentSearches: unique, most recent first, limit 10 + const existing = (req.user.recentSearches || []).filter((s) => s !== trimmed); + const next = [trimmed, ...existing].slice(0, 10); + req.user.recentSearches = next; + await req.user.save(); + res.json({ success: true, searches: next }); + } catch (err) { + next(err); + } }; \ No newline at end of file diff --git a/server/models/User.js b/server/models/User.js index c51f318..b209144 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -68,6 +68,11 @@ const userSchema = new mongoose.Schema( applied: { type: Boolean, default: false }, } ], + // Recent searches (server-side cache for signed-in users) + recentSearches: { + type: [String], + default: [], + }, }, { timestamps: true } ); diff --git a/server/routes/searchRoutes.js b/server/routes/searchRoutes.js index d52b8de..8b3e0b6 100644 --- a/server/routes/searchRoutes.js +++ b/server/routes/searchRoutes.js @@ -3,6 +3,7 @@ const rateLimit = require('express-rate-limit'); const searchController = require('../controllers/searchController'); const jwt = require('jsonwebtoken'); const User = require('../models/User'); +const { authenticateUser } = require('../middleware/auth'); const router = Router(); @@ -40,5 +41,8 @@ const optionalAuth = async (req, res, next) => { router.post('/', searchLimiter, optionalAuth, searchController.search); router.get('/suggestions', suggestionLimiter, searchController.suggestions); +// server-side recent searches (authenticated) +router.get('/recent', authenticateUser, searchController.getRecentSearches); +router.post('/recent', authenticateUser, searchController.saveRecentSearch); module.exports = router; \ No newline at end of file From d907663874b971aaf6aa4c7118b1b8f78704d7d9 Mon Sep 17 00:00:00 2001 From: Shashwat Singh <72199492+Edictor96@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:19:59 +0530 Subject: [PATCH 2/9] Add files via upload --- README.md | 194 ++++++++++++++++++++++++++++++++ product.md | 318 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 512 insertions(+) create mode 100644 README.md create mode 100644 product.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..b31d2cb --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ +# AI-Assisted FAQ & Query Resolution System + +An intelligent knowledge management platform built for internship programs. Interns get instant AI-powered answers to their questions; admins manage the knowledge base, moderate discussions, and keep the platform growing. + +--- + +## What It Does + +- Interns ask questions → AI searches the FAQ knowledge base and returns an instant answer +- If no answer exists, the query is saved and posted to the **Discussion Room** for the community to answer +- Admins moderate answers, publish official FAQs, and manage users +- Every published FAQ makes the system smarter for future users + +--- + +## Features + +### For Interns +- **FAQ Hub** — Browse all published FAQs across 14 categories +- **Hybrid Search** — Semantic + keyword search that understands intent, not just keywords +- **AI Chatbot** — Floating assistant that answers questions using the FAQ knowledge base +- **Discussion Room** — Ask questions, post answers, upvote/downvote community responses +- **Points & Leaderboard** — Earn points for contributing; compete with other interns +- **Real-time Notifications** — Get notified when someone answers your question or upvotes you +- **Programme Overview** — Live-scraped overview of the internship programme + +### For Admins +- **Moderation Panel** — Accept answers, delete posts, ban users from Discussion Room +- **FAQ Publishing** — Manually create and publish FAQs to the knowledge base +- **User Management** — View all users, promote interns to admin, manage roles +- **Analytics Dashboard** — Total users, questions, answers, FAQs at a glance + +### For Super Admins +- Everything admins can do +- **FAQ Manager** — Full control over the FAQ database +- **Audit Log** — Track all admin actions on the platform +- Assign and revoke admin roles + +--- + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Frontend | React.js, CSS Variables, custom design system | +| Backend | Node.js, Express.js | +| Database | MongoDB Atlas | +| AI — Embeddings | Google Gemini | +| AI — Responses | Groq LLM | +| Real-time | Socket.IO | +| Auth | JWT (access + refresh tokens), Google OAuth | +| Search | Hybrid semantic + keyword with in-memory embedding cache | + +--- + +## Getting Started + +### Prerequisites +- Node.js v18+ +- MongoDB Atlas account (or local MongoDB) +- Gemini API key +- Groq API key +- Google OAuth credentials + +### Installation + +**1. Clone the repo** +```bash +git clone +cd project-root +``` + +**2. Install dependencies** +```bash +# Backend +cd server +npm install + +# Frontend +cd ../client +npm install +``` + +**3. Configure environment variables** + +Create `server/.env`: +```env +PORT=3000 +NODE_ENV=development +MONGODB_URI=your_mongodb_atlas_connection_string +JWT_ACCESS_SECRET=your_access_secret +JWT_REFRESH_SECRET=your_refresh_secret +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret +CLIENT_URL=http://localhost:5173 +GEMINI_API_KEY=your_gemini_api_key +GROQ_API_KEY=your_groq_api_key +``` + +**4. Run the project** + +Open two terminals: + +```bash +# Terminal 1 — Backend +cd server +node server.js + +# Terminal 2 — Frontend +cd client +npm run dev +``` + +Visit `http://localhost:5173` + +--- + +## Roles + +| Role | Access | +|------|--------| +| `intern` | FAQ Hub, Search, Chatbot, Discussion Room, Ask Question, My Questions, Leaderboard | +| `admin` | All intern views + Admin Area (moderation, FAQ publishing, user management, analytics) | +| `super_admin` | All admin capabilities + FAQ Manager, Audit Log, full role management | + +To make a user an admin, update their `role` field in MongoDB from `intern` to `admin`. + +--- + +## Project Structure + +``` +project-root/ +├── client/ # React frontend +│ └── src/ +│ ├── api/ # Axios instance + search API +│ ├── components/ # Reusable components + layout +│ ├── context/ # Auth context +│ ├── pages/ # All page components +│ ├── services/ # API service functions +│ └── styles/ # CSS design system +│ +└── server/ # Express backend + ├── config/ # Database connection + ├── controllers/ # Route handlers + ├── middleware/ # Auth + error handling + ├── models/ # Mongoose schemas + ├── routes/ # API routes + └── services/ # Search, embeddings, scraper +``` + +--- + +## API Overview + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/auth/login` | Login with email/password | +| POST | `/api/auth/register` | Register new account | +| POST | `/api/auth/google` | Google OAuth sign-in | +| GET | `/api/faqs` | Get all published FAQs | +| GET | `/api/search` | Hybrid search across FAQs | +| POST | `/api/queries` | Save unresolved query | +| GET | `/api/questions` | Get all Discussion Room questions | +| POST | `/api/questions` | Post a new question | +| GET | `/api/questions/:id` | Get question with answers | +| POST | `/api/answers` | Post an answer | +| POST | `/api/answers/:id/vote` | Upvote or downvote an answer | +| GET | `/api/admin/stats` | Get platform analytics | +| GET | `/api/admin/users` | Get all users | +| PUT | `/api/admin/users/:id/role` | Update user role | +| GET | `/api/internship/overview` | Get programme overview | + +--- + +## Points System + +| Action | Points | +|--------|--------| +| Post an answer | +10 | +| Receive an upvote | +5 | +| Receive a downvote | -2 | +| Answer accepted by admin | +20 | + +--- + +## What's Not Built (Future) + +- Document upload via admin dashboard (PDF/TXT/DOCX → auto FAQ extraction) +- RAG pipeline for uploaded documents +- Query clustering +- n8n automation workflows +- Voice-based queries +- Mobile app diff --git a/product.md b/product.md new file mode 100644 index 0000000..53733f5 --- /dev/null +++ b/product.md @@ -0,0 +1,318 @@ +# product.md — AI-Assisted FAQ & Query Resolution System + +--- + +## 1. Problem Statement + +### What Problem Are We Solving? + +In most organizations, interns and new employees repeatedly ask the same questions. Senior staff or admins waste time answering the same query over and over. There is no structured way to capture, reuse, or grow that knowledge. + +**Core pain points:** +- People ask the same question multiple times and no one captures the answer permanently +- Admins are overwhelmed by repetitive, individual questions +- Knowledge built up over time gets lost or buried in chat logs and emails +- Traditional keyword-based search misses the intent behind a question + +### Objective + +Build an AI-powered knowledge management system where: +1. Users ask questions and get instant answers from a living knowledge base +2. Unanswered questions are saved and presented to the community for discussion +3. Admins moderate, publish official FAQs, and manage the knowledge base +4. Future users benefit from every answer ever published +5. Interns can collaborate on unresolved queries without blocking the admin workflow + +--- + +## 2. Target Users + +### Super Admin +- Full platform control +- Manage admin and intern roles +- Access audit logs and FAQ manager +- All admin capabilities + +### Admin +- Review and resolve unresolved queries +- Publish official answers as FAQs +- Manage and moderate the FAQ database +- Moderate the Discussion Room (accept/delete answers, ban users) +- Monitor system health and usage via analytics dashboard + +### Intern / User +- Simple chatbot interface to ask questions +- Instant AI-generated answers from the FAQ knowledge base +- Hybrid semantic + keyword search across FAQs +- Ability to participate in Discussion Room threads +- Earn points for contributions; appear on the Leaderboard + +--- + +## 3. Core Workflow + +``` +User asks query + ↓ +AI checks FAQ database using hybrid search (semantic + keyword) + ↓ +Answer Found? + ┌──────────────────────────┐ + │ Yes │ No + ↓ ↓ +Return AI-generated Store as unresolved query +response ↓ + Duplicate prevention check + ↓ + Saved to Discussion Room + ↓ + Community discusses + suggests answers + ↓ + Admin moderates → publishes FAQ + ↓ + New FAQ embedding auto-generated + ↓ + Future users get instant answers +``` + +--- + +## 4. Tech Stack + +### Frontend +- **React.js** — Chatbot interface, Discussion Room, Admin Dashboard, Leaderboard +- **CSS Variables / Custom Design System** — Consistent theming, animations + +### Backend +- **Node.js + Express.js** — REST API, route handling, middleware + +### Databases +- **MongoDB Atlas** — Users, FAQs, queries, discussions, answers, points +- **In-memory embedding cache** — Fast semantic search without ChromaDB overhead + +### AI / ML +- **Gemini Embeddings** — Converts text into vector representations for semantic search +- **Groq LLM** — Generates contextual AI answers from retrieved FAQ context + +### Real-time +- **Socket.IO** — Real-time notifications for answers, upvotes, and admin actions + +### Auth +- **JWT (Access + Refresh tokens)** — Secure session management +- **Google OAuth** — One-click sign-in via Google + +--- + +## 5. Implemented Features + +### Authentication +- Role-based auth: `intern`, `admin`, `super_admin` +- JWT access + refresh token flow +- Google OAuth sign-in +- Protected routes per role + +### FAQ Hub (Knowledge Base) +- All published FAQs browsable by category +- 14 categories: about-internship, certificate, code-of-conduct, coursework-vibe, interviews, noc, rosetta, selection-offer, team-formation, timing-dates, vibe-platform, work-mentorship, yaksha-chat, programme-overview +- Programme Overview section (scraped from live source with fallback) +- Trending FAQs section (top 4 shown by default) +- Category filter buttons +- Expandable FAQ accordions + +### Hybrid Search +- Semantic search using Gemini embeddings +- Keyword search fallback +- Combined hybrid scoring for best results +- Search suggestions / autocomplete +- Debounced input for performance + +### AI Chatbot (FaqAssistant) +- Floating assistant button on FAQ Hub +- Answers questions using retrieved FAQ context via Groq LLM +- Falls back gracefully when no FAQ match found + +### Unresolved Query System +- Queries with no FAQ match are saved to MongoDB +- Semantic duplicate check before saving — merges if similar entry exists +- Saved queries appear in Discussion Room + +### Discussion Room +- Reddit-style expandable threads +- Sort by: New, Top, Trending +- Search within discussions +- Inline upvote/downvote on answers +- Post answers to open questions +- Points awarded on contribution + +### Points & Leaderboard +- +10 for posting an answer +- +5 for receiving an upvote on an answer +- -2 for receiving a downvote +- +20 for answer being accepted by admin +- Leaderboard page showing top contributors + +### Real-time Notifications +- Socket.IO powered +- Notifications for: new answers on your question, upvotes received, admin actions + +### Admin Dashboard +- View and manage all users +- Role management (promote intern → admin, demote, etc.) +- Moderation panel: Accept, Delete, Ban on Discussion Room answers +- Analytics dashboard: total users, questions, answers, FAQs +- FAQ publishing (manual entry) + +### Super Admin Panel +- FAQ Manager tab +- Audit Log tab +- Full role management including assigning admin roles +- All admin capabilities + +### Navigation (Role-based Sidebar) +- **Intern:** FAQ Hub, Dashboard, Ask Question, My Questions, Discussion Room, Leaderboard +- **Admin / Super Admin:** FAQ Hub, Dashboard, Discussion Room, Leaderboard, Admin Area + +--- + +## 6. Database Design + +### MongoDB Collections + +| Collection | Stores | +|------------|--------| +| `users` | User accounts, roles, points, auth tokens | +| `faqs` | Published FAQs with question, answer, category, timestamps | +| `queries` | Unresolved user queries | +| `questions` | Discussion Room questions with status | +| `answers` | Answers to Discussion Room questions with votes | +| `notifications` | Real-time notification records per user | + +--- + +## 7. Project Structure + +``` +project-root/ +│ +├── client/ +│ ├── src/ +│ │ ├── api/ +│ │ │ ├── axios.js +│ │ │ └── searchApi.js +│ │ ├── components/ +│ │ │ ├── layout/ +│ │ │ │ ├── DashboardLayout.jsx +│ │ │ │ ├── Sidebar.jsx +│ │ │ │ └── Header.jsx +│ │ │ ├── FaqAssistant.jsx +│ │ │ ├── SearchBar.jsx +│ │ │ ├── SearchSuggestions.jsx +│ │ │ ├── GoogleSignInButton.jsx +│ │ │ └── ProtectedRoute.jsx +│ │ ├── context/ +│ │ │ └── AuthContext.jsx +│ │ ├── pages/ +│ │ │ ├── UserPage.jsx (FAQ Hub) +│ │ │ ├── DashboardHome.jsx +│ │ │ ├── AskQuestion.jsx +│ │ │ ├── MyQuestions.jsx +│ │ │ ├── QuestionDetail.jsx +│ │ │ ├── AnswerCenter.jsx (Discussion Room) +│ │ │ ├── AdminArea.jsx +│ │ │ ├── QueryPage.jsx +│ │ │ ├── Leaderboard.jsx +│ │ │ ├── LoginPage.jsx +│ │ │ └── RegisterPage.jsx +│ │ ├── services/ +│ │ │ ├── authService.js +│ │ │ ├── faqService.js +│ │ │ ├── questionService.js +│ │ │ ├── answerService.js +│ │ │ └── adminService.js +│ │ └── styles/ +│ +├── server/ +│ ├── config/ +│ │ └── db.js +│ ├── controllers/ +│ │ ├── authController.js +│ │ ├── faqController.js +│ │ ├── queryController.js +│ │ ├── questionController.js +│ │ ├── answerController.js +│ │ └── adminController.js +│ ├── middleware/ +│ │ ├── auth.js +│ │ └── errorHandler.js +│ ├── models/ +│ │ ├── User.js +│ │ ├── Faq.js +│ │ ├── Query.js +│ │ ├── Question.js +│ │ ├── Answer.js +│ │ └── Notification.js +│ ├── routes/ +│ │ ├── authRoutes.js +│ │ ├── faqRoutes.js +│ │ ├── queryRoutes.js +│ │ ├── questionRoutes.js +│ │ ├── answerRoutes.js +│ │ ├── adminRoutes.js +│ │ ├── searchRoutes.js +│ │ └── internshipRoutes.js +│ ├── services/ +│ │ ├── searchService.js +│ │ ├── embeddingService.js +│ │ ├── scraper.js +│ │ └── internshipFaqs.js +│ └── server.js +│ +├── docs/ +│ ├── product.md +│ └── README.md +│ +└── .env +``` + +--- + +## 8. Feature Checklist + +| Feature | Status | +|---------|--------| +| Role-based authentication (intern/admin/super_admin) | ✅ Built | +| JWT access + refresh token flow | ✅ Built | +| Google OAuth sign-in | ✅ Built | +| FAQ Hub with category filtering | ✅ Built | +| Programme Overview section | ✅ Built | +| Hybrid search (semantic + keyword) | ✅ Built | +| Search suggestions / autocomplete | ✅ Built | +| AI chatbot (FaqAssistant) | ✅ Built | +| Unresolved query saving | ✅ Built | +| Semantic duplicate prevention | ✅ Built | +| Discussion Room with threads | ✅ Built | +| Upvote / downvote on answers | ✅ Built | +| Points system | ✅ Built | +| Leaderboard | ✅ Built | +| Real-time notifications (Socket.IO) | ✅ Built | +| Admin moderation panel | ✅ Built | +| Analytics dashboard | ✅ Built | +| Super admin panel (FAQ Manager + Audit Log) | ✅ Built | +| Role-based sidebar navigation | ✅ Built | +| MongoDB Atlas | ✅ Connected | +| Document upload + FAQ extraction | 🔲 Not built | +| Query clustering | 🔲 Removed from scope | +| n8n automation workflows | 🔲 Removed from scope | + +--- + +## 9. Future Enhancements + +- Document upload via admin dashboard (PDF/TXT/DOCX → auto FAQ extraction) +- RAG pipeline for uploaded documents directly from the admin dashboard +- Multi-language semantic search +- Voice-based queries (speech-to-text) +- AI-generated FAQ suggestions for admin approval +- Real-time co-editing on FAQ drafts +- Mobile app (React Native) +- \ No newline at end of file From 082719258fe85da50b3846bcbadc4abce51f829a Mon Sep 17 00:00:00 2001 From: Shashwat Singh <72199492+Edictor96@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:32:14 +0530 Subject: [PATCH 3/9] Update product.md --- product.md | 1 - 1 file changed, 1 deletion(-) diff --git a/product.md b/product.md index 53733f5..4b5af7f 100644 --- a/product.md +++ b/product.md @@ -315,4 +315,3 @@ project-root/ - AI-generated FAQ suggestions for admin approval - Real-time co-editing on FAQ drafts - Mobile app (React Native) -- \ No newline at end of file From 539b6dd9bd4c966abe0ae066b91306484b4f474e Mon Sep 17 00:00:00 2001 From: Shashwat Singh <72199492+Edictor96@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:25:37 +0530 Subject: [PATCH 4/9] Deployment Fixes --- client/src/context/SocketContext.jsx | 2 +- client/vercel.json | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 client/vercel.json diff --git a/client/src/context/SocketContext.jsx b/client/src/context/SocketContext.jsx index 42453ad..2603f53 100644 --- a/client/src/context/SocketContext.jsx +++ b/client/src/context/SocketContext.jsx @@ -20,7 +20,7 @@ export function SocketProvider({ children }) { return; } - const SOCKET_URL = 'http://localhost:3000'; + const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || window.location.origin; const socket = io(SOCKET_URL, { auth: { token }, transports: ['websocket', 'polling'], diff --git a/client/vercel.json b/client/vercel.json new file mode 100644 index 0000000..538374c --- /dev/null +++ b/client/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} \ No newline at end of file From be6493234d8227fd3710c9cfeccac76b6d12dd70 Mon Sep 17 00:00:00 2001 From: Shashwat Singh <72199492+Edictor96@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:51:31 +0530 Subject: [PATCH 5/9] Debugs --- .env.example | 29 ++++++++++++++ README.md | 40 +++++++++++++++++++- client/.env.example | 5 +++ client/package.json | 3 ++ client/src/api/axios.js | 6 ++- client/src/context/SocketContext.jsx | 2 +- client/vercel.json | 11 +++++- server/.env.example | 26 ++++++++++--- server/config/env.js | 30 +++++++++++++++ server/controllers/authController.js | 34 ++++++++--------- server/package.json | 3 ++ server/routes/authRoutes.js | 22 +---------- server/routes/questionRoutes.js | 2 +- server/server.js | 56 ++++++++++------------------ server/services/socketService.js | 3 +- 15 files changed, 185 insertions(+), 87 deletions(-) create mode 100644 .env.example create mode 100644 client/.env.example create mode 100644 server/config/env.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4b346e6 --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +# Root environment template for local development and deployment. +# Use client/.env for Vite variables and server/.env for backend variables. + +# ---------------------- +# Client (Vite) +# ---------------------- +VITE_API_BASE_URL=http://localhost:3000/api +VITE_SOCKET_URL=http://localhost:3000 + +# ---------------------- +# Server (Express) +# ---------------------- +NODE_ENV=development +PORT=3000 +MONGODB_URI=mongodb://localhost:27017/faq_auth +JWT_ACCESS_SECRET=replace-with-a-long-random-secret +JWT_REFRESH_SECRET=replace-with-a-long-random-secret +GOOGLE_CLIENT_ID=your-google-oauth-client-id +GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret +CLIENT_URL=http://localhost:5173 +CLIENT_URLS=http://localhost:5173,http://localhost:5174 +LLM_ENDPOINT=https://api.openai.com/v1/chat/completions +LLM_API_KEY= +OPENAI_API_KEY= +LLM_MODEL=gpt-4o-mini +CHROMA_HOST=localhost +CHROMA_PORT=8000 +CHROMA_COLLECTION=faq_embeddings +POINTS_JOB_INTERVAL_MS=3600000 diff --git a/README.md b/README.md index b31d2cb..7d1000a 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,20 @@ JWT_REFRESH_SECRET=your_refresh_secret GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret CLIENT_URL=http://localhost:5173 -GEMINI_API_KEY=your_gemini_api_key -GROQ_API_KEY=your_groq_api_key +CLIENT_URLS=http://localhost:5173,http://localhost:5174 +LLM_ENDPOINT=https://api.openai.com/v1/chat/completions +LLM_API_KEY=your_llm_api_key +LLM_MODEL=gpt-4o-mini +CHROMA_HOST=localhost +CHROMA_PORT=8000 +CHROMA_COLLECTION=faq_embeddings +POINTS_JOB_INTERVAL_MS=3600000 +``` + +Create `client/.env`: +```env +VITE_API_BASE_URL=http://localhost:3000/api +VITE_SOCKET_URL=http://localhost:3000 ``` **4. Run the project** @@ -173,6 +185,30 @@ project-root/ --- +## Vercel Deployment (Client) + +This repository is a split architecture: + +- `client` is a Vite SPA suitable for Vercel static hosting. +- `server` is a long-running Node + Socket.IO API and should be hosted on a persistent backend platform (Render, Railway, Fly, VM, etc.), not Vercel serverless. + +### Vercel project settings + +- Framework Preset: `Vite` +- Root Directory: `client` +- Install Command: `npm install` +- Build Command: `npm run build` +- Output Directory: `dist` + +### Required Vercel environment variables (client) + +- `VITE_API_BASE_URL=https:///api` +- `VITE_SOCKET_URL=https://` + +The SPA fallback rewrite is configured in `client/vercel.json` and excludes `/api` and `/socket.io` paths so API traffic is not rewritten to HTML. + +--- + ## Points System | Action | Points | diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 0000000..8511b20 --- /dev/null +++ b/client/.env.example @@ -0,0 +1,5 @@ +# Vite client environment variables +# Local dev defaults to Vite proxy when VITE_API_BASE_URL is omitted. + +VITE_API_BASE_URL=http://localhost:3000/api +VITE_SOCKET_URL=http://localhost:3000 diff --git a/client/package.json b/client/package.json index 2ee4f31..112d1de 100644 --- a/client/package.json +++ b/client/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "type": "module", + "engines": { + "node": ">=18.18.0" + }, "scripts": { "dev": "vite", "build": "vite build", diff --git a/client/src/api/axios.js b/client/src/api/axios.js index 0fda297..f5c8f19 100644 --- a/client/src/api/axios.js +++ b/client/src/api/axios.js @@ -1,7 +1,9 @@ import axios from 'axios'; +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'; + const api = axios.create({ - baseURL: '/api', + baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json' }, withCredentials: true, }); @@ -55,7 +57,7 @@ api.interceptors.response.use( } try { - const { data } = await axios.post('/api/auth/refresh', { refreshToken }); + const { data } = await axios.post(`${API_BASE_URL}/auth/refresh`, { refreshToken }); const storage = localStorage.getItem('rememberMe') === 'true' ? localStorage : sessionStorage; storage.setItem('accessToken', data.accessToken); diff --git a/client/src/context/SocketContext.jsx b/client/src/context/SocketContext.jsx index 2603f53..08e2d75 100644 --- a/client/src/context/SocketContext.jsx +++ b/client/src/context/SocketContext.jsx @@ -20,7 +20,7 @@ export function SocketProvider({ children }) { return; } - const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || window.location.origin; + const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || (import.meta.env.DEV ? 'http://localhost:3000' : window.location.origin); const socket = io(SOCKET_URL, { auth: { token }, transports: ['websocket', 'polling'], diff --git a/client/vercel.json b/client/vercel.json index 538374c..6672e48 100644 --- a/client/vercel.json +++ b/client/vercel.json @@ -1,7 +1,16 @@ { + "cleanUrls": true, "rewrites": [ { - "source": "/(.*)", + "source": "/api", + "destination": "/api" + }, + { + "source": "/socket.io", + "destination": "/socket.io" + }, + { + "source": "/((?!api/|socket.io/).*)", "destination": "/index.html" } ] diff --git a/server/.env.example b/server/.env.example index f580c32..d12a724 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,8 +1,24 @@ -JWT_ACCESS_SECRET=your-access-secret-min-32-chars -JWT_REFRESH_SECRET=your-refresh-secret-min-32-chars +NODE_ENV=development +PORT=3000 + +MONGODB_URI=mongodb://localhost:27017/faq_auth + +JWT_ACCESS_SECRET=replace-with-a-long-random-secret +JWT_REFRESH_SECRET=replace-with-a-long-random-secret + GOOGLE_CLIENT_ID=your-google-oauth-client-id GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret -MONGODB_URI=mongodb://localhost:27017/faq_auth -PORT=3000 -NODE_ENV=development + CLIENT_URL=http://localhost:5173 +CLIENT_URLS=http://localhost:5173,http://localhost:5174 + +LLM_ENDPOINT=https://api.openai.com/v1/chat/completions +LLM_API_KEY= +OPENAI_API_KEY= +LLM_MODEL=gpt-4o-mini + +CHROMA_HOST=localhost +CHROMA_PORT=8000 +CHROMA_COLLECTION=faq_embeddings + +POINTS_JOB_INTERVAL_MS=3600000 diff --git a/server/config/env.js b/server/config/env.js new file mode 100644 index 0000000..3283f4b --- /dev/null +++ b/server/config/env.js @@ -0,0 +1,30 @@ +const requiredEnv = [ + 'MONGODB_URI', + 'JWT_ACCESS_SECRET', + 'JWT_REFRESH_SECRET', + 'CLIENT_URL', +]; + +function validateEnv() { + const missing = requiredEnv.filter((key) => !process.env[key] || !String(process.env[key]).trim()); + + if (missing.length > 0) { + throw new Error(`Missing required environment variable(s): ${missing.join(', ')}`); + } +} + +function getAllowedOrigins() { + const raw = process.env.CLIENT_URLS || process.env.CLIENT_URL || ''; + const origins = raw + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + + if (origins.length === 0) { + origins.push('http://localhost:5173'); + } + + return origins; +} + +module.exports = { validateEnv, getAllowedOrigins }; diff --git a/server/controllers/authController.js b/server/controllers/authController.js index 226cfa3..3f382d6 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -28,20 +28,20 @@ const sendTokens = (user, statusCode, res) => { const refreshToken = signRefreshToken(user._id); user.refreshToken = refreshToken; - user.save(); - - res.status(statusCode).json({ - success: true, - accessToken, - refreshToken, - user: { - id: user._id, - name: user.name, - email: user.email, - role: user.role, - isVerified: user.isVerified, - points: user.points || 0, - }, + return user.save().then(() => { + res.status(statusCode).json({ + success: true, + accessToken, + refreshToken, + user: { + id: user._id, + name: user.name, + email: user.email, + role: user.role, + isVerified: user.isVerified, + points: user.points || 0, + }, + }); }); }; @@ -74,7 +74,7 @@ exports.register = async (req, res, next) => { notifyUser(admin._id, notif); } - sendTokens(user, 201, res); + await sendTokens(user, 201, res); } catch (err) { next(err); } @@ -107,7 +107,7 @@ exports.login = async (req, res, next) => { user.resetLoginAttempts(); await user.save(); - sendTokens(user, 200, res); + await sendTokens(user, 200, res); } catch (err) { next(err); } @@ -151,7 +151,7 @@ exports.googleAuth = async (req, res, next) => { }); } - sendTokens(user, 200, res); + await sendTokens(user, 200, res); } catch (err) { next(err); } diff --git a/server/package.json b/server/package.json index cc93766..8e2c0f8 100644 --- a/server/package.json +++ b/server/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "description": "Production-ready authentication API for FAQ Management Platform", "main": "server.js", + "engines": { + "node": ">=18.18.0" + }, "scripts": { "start": "node server.js", "dev": "node --watch server.js" diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index 298d62b..0af0d3a 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -1,7 +1,7 @@ const { Router } = require('express'); const rateLimit = require('express-rate-limit'); const authController = require('../controllers/authController'); -const { authenticateUser, authorizeRoles } = require('../middleware/auth'); +const { authenticateUser } = require('../middleware/auth'); const { validate, registerSchema, @@ -39,24 +39,4 @@ router.put('/reset-password/:token', validate(resetPasswordSchema), authControll router.get('/me', authenticateUser, authController.getMe); router.put('/profile', authenticateUser, authController.updateProfile); -router.post('/seed-admin', async (req, res) => { - try { - const User = require('../models/User'); - const existing = await User.findOne({ email: 'sudarshansudarshan@gmail.com' }); - if (existing) { - return res.json({ success: true, message: 'Admin already exists', email: existing.email, role: existing.role }); - } - const admin = await User.create({ - name: 'Sudarshan Admin', - email: 'sudarshansudarshan@gmail.com', - password: 'Admin@123', - role: 'super_admin', - isVerified: true, - }); - res.status(201).json({ success: true, message: 'Super admin created', email: admin.email }); - } catch (err) { - res.status(400).json({ success: false, message: err.message }); - } -}); - module.exports = router; diff --git a/server/routes/questionRoutes.js b/server/routes/questionRoutes.js index f12e30b..81e6612 100644 --- a/server/routes/questionRoutes.js +++ b/server/routes/questionRoutes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { createQuestion, getQuestions, getQuestionById, getMyQuestions, deleteQuestion } = require('../controllers/questionController'); const { authenticateUser: protect, authorizeRoles } = require('../middleware/auth'); -const admin = authorizeRoles('admin'); +const admin = authorizeRoles('admin', 'super_admin'); router.route('/') .post(protect, createQuestion) diff --git a/server/server.js b/server/server.js index 70321f1..d0b8b78 100644 --- a/server/server.js +++ b/server/server.js @@ -23,6 +23,9 @@ const notificationRoutes = require('./routes/notificationRoutes'); const { indexAllFaqs } = require('./services/searchService'); const { errorHandler } = require('./middleware/errorHandler'); const { setupSocket } = require('./services/socketService'); +const { validateEnv, getAllowedOrigins } = require('./config/env'); + +validateEnv(); const app = express(); @@ -39,7 +42,7 @@ app.use(helmet({ }, })); app.use(cors({ - origin: [process.env.CLIENT_URL || 'http://localhost:5173', 'http://localhost:5174'], + origin: getAllowedOrigins(), credentials: true, })); app.use(cookieParser()); @@ -115,22 +118,12 @@ const seedFAQs = async () => { const server = http.createServer(app); setupSocket(server); -const startServer = async () => { - try { - await connectDB(); - await seedFAQs(); - indexAllFaqs().then(count => { - if (count > 0) console.log(`Search: Indexed ${count} FAQs for semantic search`); - }).catch(() => {}); - server.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); - }); -// Apply pending points every hour -setInterval(async () => { +const runPendingPointsJob = async () => { try { const User = require('./models/User'); const now = new Date(); const users = await User.find({ 'pointsHistory.applied': false }); + for (const user of users) { let changed = false; for (const entry of user.pointsHistory) { @@ -146,7 +139,20 @@ setInterval(async () => { } catch (e) { console.warn('Points job error:', e.message); } -}, 60 * 60 * 1000); +}; + +const startServer = async () => { + try { + await connectDB(); + await seedFAQs(); + indexAllFaqs().then(count => { + if (count > 0) console.log(`Search: Indexed ${count} FAQs for semantic search`); + }).catch(() => {}); + server.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); + }); + const pointsJobMs = Number(process.env.POINTS_JOB_INTERVAL_MS) || 60 * 60 * 1000; + setInterval(runPendingPointsJob, pointsJobMs); } catch (err) { console.error('Failed to start server:', err.message); @@ -155,25 +161,3 @@ setInterval(async () => { }; startServer(); - -setInterval(async () => { - try { - const User = require('./models/User'); - const now = new Date(); - const users = await User.find({ 'pointsHistory.applied': false }); - - for (const user of users) { - let changed = false; - for (const entry of user.pointsHistory) { - if (!entry.applied && entry.appliedAt <= now) { - user.points = Math.max(0, (user.points || 0) + entry.points); - entry.applied = true; - changed = true; - } - } - if (changed) await user.save(); - } - } catch (e) { - console.warn('Points job error:', e.message); - } -}, 10 * 1000); diff --git a/server/services/socketService.js b/server/services/socketService.js index c7f2a4c..036ab90 100644 --- a/server/services/socketService.js +++ b/server/services/socketService.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const User = require('../models/User'); +const { getAllowedOrigins } = require('../config/env'); let io = null; @@ -10,7 +11,7 @@ function setupSocket(server) { io = new Server(server, { cors: { - origin: [process.env.CLIENT_URL || 'http://localhost:5173', 'http://localhost:5174'], + origin: getAllowedOrigins(), credentials: true, }, }); From 342f3c1c84e20dd6f997c37e6b98f472298bd1cf Mon Sep 17 00:00:00 2001 From: Shashwat Singh <72199492+Edictor96@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:55:14 +0530 Subject: [PATCH 6/9] Add files via upload --- .env | 29 +++++++++++++++++++++++++++++ package-lock.json | 6 ++++++ 2 files changed, 35 insertions(+) create mode 100644 .env create mode 100644 package-lock.json diff --git a/.env b/.env new file mode 100644 index 0000000..4b346e6 --- /dev/null +++ b/.env @@ -0,0 +1,29 @@ +# Root environment template for local development and deployment. +# Use client/.env for Vite variables and server/.env for backend variables. + +# ---------------------- +# Client (Vite) +# ---------------------- +VITE_API_BASE_URL=http://localhost:3000/api +VITE_SOCKET_URL=http://localhost:3000 + +# ---------------------- +# Server (Express) +# ---------------------- +NODE_ENV=development +PORT=3000 +MONGODB_URI=mongodb://localhost:27017/faq_auth +JWT_ACCESS_SECRET=replace-with-a-long-random-secret +JWT_REFRESH_SECRET=replace-with-a-long-random-secret +GOOGLE_CLIENT_ID=your-google-oauth-client-id +GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret +CLIENT_URL=http://localhost:5173 +CLIENT_URLS=http://localhost:5173,http://localhost:5174 +LLM_ENDPOINT=https://api.openai.com/v1/chat/completions +LLM_API_KEY= +OPENAI_API_KEY= +LLM_MODEL=gpt-4o-mini +CHROMA_HOST=localhost +CHROMA_PORT=8000 +CHROMA_COLLECTION=faq_embeddings +POINTS_JOB_INTERVAL_MS=3600000 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fc9d434 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "CSFAQ-Sphinx-main", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 83a795bfcc28821e05eb5762e36e4046e0a097b3 Mon Sep 17 00:00:00 2001 From: Shashwat Singh <72199492+Edictor96@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:07:48 +0530 Subject: [PATCH 7/9] Delete .env.example --- .env.example | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 4b346e6..0000000 --- a/.env.example +++ /dev/null @@ -1,29 +0,0 @@ -# Root environment template for local development and deployment. -# Use client/.env for Vite variables and server/.env for backend variables. - -# ---------------------- -# Client (Vite) -# ---------------------- -VITE_API_BASE_URL=http://localhost:3000/api -VITE_SOCKET_URL=http://localhost:3000 - -# ---------------------- -# Server (Express) -# ---------------------- -NODE_ENV=development -PORT=3000 -MONGODB_URI=mongodb://localhost:27017/faq_auth -JWT_ACCESS_SECRET=replace-with-a-long-random-secret -JWT_REFRESH_SECRET=replace-with-a-long-random-secret -GOOGLE_CLIENT_ID=your-google-oauth-client-id -GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret -CLIENT_URL=http://localhost:5173 -CLIENT_URLS=http://localhost:5173,http://localhost:5174 -LLM_ENDPOINT=https://api.openai.com/v1/chat/completions -LLM_API_KEY= -OPENAI_API_KEY= -LLM_MODEL=gpt-4o-mini -CHROMA_HOST=localhost -CHROMA_PORT=8000 -CHROMA_COLLECTION=faq_embeddings -POINTS_JOB_INTERVAL_MS=3600000 From 900b36961f7f9a017435bdab2b8a916bfee4bcd4 Mon Sep 17 00:00:00 2001 From: Shashwat Singh <72199492+Edictor96@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:09:20 +0530 Subject: [PATCH 8/9] Add files via upload --- client/package-lock.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/package-lock.json b/client/package-lock.json index 15985db..ca9f6f2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -18,6 +18,9 @@ "devDependencies": { "@vitejs/plugin-react": "^4.3.1", "vite": "^6.0.0" + }, + "engines": { + "node": ">=18.18.0" } }, "node_modules/@babel/code-frame": { From 716376f46434847306424c1ba0bec5b2b27fbe6e Mon Sep 17 00:00:00 2001 From: Shashwat Singh <72199492+Edictor96@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:10:35 +0530 Subject: [PATCH 9/9] Add files via upload --- client/src/pages/QueryPage.jsx | 213 +++++++++++++++++++++------------ 1 file changed, 139 insertions(+), 74 deletions(-) diff --git a/client/src/pages/QueryPage.jsx b/client/src/pages/QueryPage.jsx index 34680ed..45cede6 100644 --- a/client/src/pages/QueryPage.jsx +++ b/client/src/pages/QueryPage.jsx @@ -81,78 +81,143 @@ export default function QueryPage() { }; return ( -
-
-

Submit a Query

-

- Didn't find what you were looking for? Let us know. -

- -
-
- - setForm((p) => ({ ...p, question: e.target.value }))} - style={inputStyle} - required - /> -
- -
- - -
- -
- -