diff --git a/backend/.env.example b/backend/.env.example index e36f31a..9157318 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,7 +1,8 @@ JWT_SECRET= REFRESH_SECRET= SALTING= -API_URL='http://localhost:4000/api/v1' +API_URL= +GEMINI_API_KEY= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_CALLBACK_URL= diff --git a/backend/bun.lock b/backend/bun.lock index 7ab1e6a..92972ef 100644 --- a/backend/bun.lock +++ b/backend/bun.lock @@ -1,9 +1,11 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "backend", "dependencies": { + "@google/generative-ai": "^0.24.1", "@types/axios": "^0.14.4", "@types/bcrypt": "^6.0.0", "@types/cookie-parser": "^1.4.10", @@ -24,6 +26,7 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", + "node-fetch": "^3.3.2", "passport": "^0.7.0", "resend": "^6.5.2", "zod": "^4.0.9", @@ -152,6 +155,8 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.4", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw=="], + "@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="], + "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -520,6 +525,8 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="], @@ -626,6 +633,8 @@ "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], @@ -646,6 +655,8 @@ "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -900,6 +911,10 @@ "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], @@ -1140,6 +1155,8 @@ "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], diff --git a/backend/package.json b/backend/package.json index 3cddca7..4fceeec 100644 --- a/backend/package.json +++ b/backend/package.json @@ -39,6 +39,7 @@ "typescript": "^5.8.3" }, "dependencies": { + "@google/generative-ai": "^0.24.1", "@types/axios": "^0.14.4", "@types/bcrypt": "^6.0.0", "@types/cookie-parser": "^1.4.10", @@ -59,6 +60,7 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", + "node-fetch": "^3.3.2", "passport": "^0.7.0", "resend": "^6.5.2", "zod": "^4.0.9" diff --git a/backend/src/ai/chatBrain.ts b/backend/src/ai/chatBrain.ts new file mode 100644 index 0000000..063e5ed --- /dev/null +++ b/backend/src/ai/chatBrain.ts @@ -0,0 +1,142 @@ +const formatHistory = (history: { from: string; text: string }[] = []) => + history + .map((m) => `${m.from === "user" ? "User" : "Assistant"}: ${m.text}`) + .join("\n"); + +export function buildPrompt({ + path, + ragData, + userAnswer, + questionContext, + history = [], +}: { + path: string; + ragData: any; + userAnswer: string; + questionContext?: any; + history?: { from: string; text: string }[]; +}) { + const baseRole = ` +You are Cortex, an intelligent AI analyst for "Call of Code". +STRICT LIMITATION: Your knowledge is limited EXCLUSIVELY to DSA and Interview Experiences. + +🎯 YOUR GOALS: +1. If user says hi or hello, greet them back briefly and ask how you can assist in DSA/Interview prep. +2. Provide accurate, concise, and relevant answers ONLY related to DSA problems or interview experiences. +3. Politely refuse to answer anything outside DSA/Interviews with the Refusal Message below. + +💡 SPECIAL INSTRUCTIONS BASED ON CONTEXT: +1. DSA HELP (3-Step Method): + - Start with a **simple hint** only. + - If user asks for solution (e.g., "give code", "solve it"), provide: (a) Brute-force logic + code, (b) Optimal solution + code. + - Suggest 2-3 similar practice problems from LeetCode/GFG/CodeChef. +2. CAREER QUESTIONS: Define the role, list skills, give a step-by-step roadmap, and suggest learning platforms. +3. INTERVIEW ANALYSIS: Provide direct answers based on stories. DO NOT give hints for interview data queries. + +RULES: +- If the query is NOT about DSA/Interviews, use the Refusal Message: "I am Cortex, specialized only in DSA and Interview analysis. Let's stay focused on your preparation!" +- Do NOT start a mock interview. Do NOT act as Alex. +- Use Markdown (bold, headers, code blocks) and emojis 🚀. +`; + + console.log("🧠 QUESTION CONTEXT IN chatBrain 👉", questionContext); + + // --- CASE 1: DSA TOPIC ONLY --- +if (questionContext?.type === "DSA" && questionContext.isTopicOnly) { + const questionsList = ragData?.relatedQuestions + ? ragData.relatedQuestions.map((q: any) => `- ${q.title}`).join("\n") + : "No questions listed for this topic yet."; + + return ` +${baseRole} +User is browsing: ${questionContext.topicTitle} +Available Questions in this topic: +${questionsList} + +Instructions: +- Briefly explain the importance of ${questionContext.topicTitle} in interviews. +- If user asks what to solve, suggest questions from the list above. + +User Query: ${userAnswer} +`; +} + + // --- CASE 2: SPECIFIC DSA QUESTION --- + if (questionContext?.type === "DSA" && questionContext.questionId) { + const context = ragData ? `QUESTION: ${ragData.question}\nCONCEPT: ${ragData.concept}` : "No specific details available."; + + return ` +${baseRole} +Conversation so far: +${formatHistory(history)} + +CURRENT QUESTION: ${questionContext.questionName} (${questionContext.topicTitle}) +${context} + +USER ANSWER / CODE: +${userAnswer} + +Instructions: +- Give ONLY a hint first. +- Point out logical mistakes. +`; + } + + // --- CASE 3: INTERVIEW ANALYST (COLLECTION) --- + if (questionContext?.type === "INTERVIEW_COLLECTION") { + // Check if ragData exists and has items + const allExperiences = (Array.isArray(ragData) && ragData.length > 0) + ? ragData.slice(0, 10).map((exp: any) => // Top 10 for performance + `Student: ${exp.member?.name || "User"} | Company: ${exp.company} | Verdict: ${exp.verdict} | Summary: ${exp.role}` + ).join("\n") + : "No specific interview stories found in the database."; + + return ` +${baseRole} +CONTEXT: The user is looking at the Interview Experiences page. +DATABASE SUMMARY (Last few stories): +${allExperiences} + +User Question: ${userAnswer} + +Instructions: +- Analyze the common patterns in the stories provided. +- Identify common mistakes leading to "Rejected" verdicts. +- Determine which companies focused more on DSA. +- If they ask for details of a specific person, tell them to click on that card for a deep-dive analysis. +`; + } + + // --- CASE 4: SINGLE INTERVIEW EXPERIENCE --- +if (questionContext?.type === "INTERVIEW_EXPERIENCE") { + const current = ragData?.currentInterview || questionContext; + const content = ragData?.currentInterview?.content || "Story content loading..."; + const studentName = current.member?.name || current.studentName || "the candidate"; + + return ` +${baseRole} +CONTEXT: Interview for ${current.company}. +CANDIDATE: ${studentName} | VERDICT: ${current.verdict} +DATA: """${content}""" + +INSTRUCTIONS: +1. If the user says "hi" or "hello", greet them professionally and ask what they want to know specifically. +2. Provide the FULL ANALYSIS ONLY if the user asks a specific question (e.g., "what rounds?", "questions?", "give analysis"). +3. Use this format for analysis: + 🔍 **Analysis: ${current.company} Journey** + - **The Process**: ... + - **Technical Deep-Dive**: ... + - **The Verdict Factor**: ... + +User Query: ${userAnswer} +`; +} + + // --- CASE 5: DEFAULT FALLBACK --- + return ` +${baseRole} +Conversation so far: +${formatHistory(history)} +User message: ${userAnswer} +`; +} \ No newline at end of file diff --git a/backend/src/app.ts b/backend/src/app.ts index 7a167e8..9a9aef1 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -8,6 +8,9 @@ import { errorHandler } from './utils/apiError'; import rateLimit from 'express-rate-limit'; import helmet from 'helmet'; import cookieParser from 'cookie-parser' +import chatRoutes from "./routes/chat.route"; + + const app = express(); @@ -32,6 +35,8 @@ app.use(limiter) app.use(cookieParser()); app.use(json()); app.use(urlencoded({ extended: true })); +app.use(express.json()); +app.use("/chat", chatRoutes); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 2 * 1024 * 1024 } diff --git a/backend/src/controllers/chatController.ts b/backend/src/controllers/chatController.ts new file mode 100644 index 0000000..4aa734d --- /dev/null +++ b/backend/src/controllers/chatController.ts @@ -0,0 +1,59 @@ +import { Request, Response } from "express"; +import { fetchRAGContext } from "../services/retrival"; +import { buildPrompt } from "../ai/chatBrain"; +import { geminiModel } from "../services/ai"; + +export async function chatController(req: Request, res: Response) { + try { + const { + path, + userAnswer, + questionContext, + history = [], // Added default value + } = req.body; + + // Fetch RAG data + const ragData = await fetchRAGContext(questionContext); + + + if (ragData) { + console.log(` Context Loaded: ${ragData.company || ragData.topicTitle || 'General'}`); + } + + const prompt = buildPrompt({ + path, + ragData, + userAnswer, + questionContext, + history, + }); + + // Call Gemini + const chat = geminiModel.startChat({ + history: [], + }); + + try { + const result = await chat.sendMessage(prompt); + const reply = result.response.text(); + return res.json({ reply }); // Explicit return + } catch (aiError: any) { + // ⚠️ Handle Gemini Specific Overload (503) + if (aiError.status === 503 || aiError.message?.includes("503")) { + return res.status(503).json({ + reply: "Google's AI servers are a bit busy 🧠⚡. Please wait 5-10 seconds and try again!", + }); + } + throw aiError; // Pass to main catch + } + + } catch (error) { + console.error("🚨 Chat Controller Error:", error); + const err = error as Error; + + res.status(500).json({ + reply: "Cortex encountered an issue. Let's try that again in a moment.", + error: process.env.NODE_ENV === 'development' ? err.message : undefined // Hide technical error in production + }); + } +} \ No newline at end of file diff --git a/backend/src/routes/chat.route.ts b/backend/src/routes/chat.route.ts new file mode 100644 index 0000000..a4321f2 --- /dev/null +++ b/backend/src/routes/chat.route.ts @@ -0,0 +1,8 @@ +import { Router } from "express"; +import { chatController } from "../controllers/chatController"; + +const chatRouter = Router(); + +chatRouter.post("/", chatController); + +export default chatRouter; diff --git a/backend/src/services/ai.ts b/backend/src/services/ai.ts new file mode 100644 index 0000000..f33f8d1 --- /dev/null +++ b/backend/src/services/ai.ts @@ -0,0 +1,7 @@ +import { GoogleGenerativeAI } from "@google/generative-ai"; + +const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); + +export const geminiModel = genAI.getGenerativeModel({ + model: "gemini-2.5-flash", +}); diff --git a/backend/src/services/retrival.ts b/backend/src/services/retrival.ts new file mode 100644 index 0000000..b9b7ceb --- /dev/null +++ b/backend/src/services/retrival.ts @@ -0,0 +1,59 @@ +import api from "../utils/api"; + +export async function fetchRAGContext(questionContext?: any) { + if (!questionContext) return null; + + try { + // --- CASE 1: DSA (Same as before, working fine) --- + if (questionContext.type === "DSA") { + if (questionContext.questionId) { + const res = await api.get(`/dsa/questions/${questionContext.questionId}`); + return res.data?.data || null; + } + + if (questionContext.isTopicOnly && questionContext.topicId) { + const res = await api.get(`/topics/${questionContext.topicId}/questions`); + const questions = res.data?.questions || []; + return { + ...questionContext, + relatedQuestions: questions, + topicOverview: `This topic contains ${questions.length} practice questions.` + }; + } + } + + // --- CASE 2 & 3: INTERVIEW SYSTEM (The finale Fix) --- + if (questionContext.type === "INTERVIEW_EXPERIENCE") { + const specificRes = await api.get(`/interviews/${questionContext.id}`); + const specificData = specificRes.data?.data; + + + + const globalRes = await api.get(`/interviews?limit=20`); + const allInterviews = globalRes.data?.data || []; + + return { + currentInterview: specificData, + otherInterviews: allInterviews.map((i: any) => ({ + student: i.member?.name || "Anonymous", + company: i.company, + verdict: i.verdict, + id: i._id + })), + contextMeta: questionContext + }; + } + + // Default Case + if (questionContext.type === "INTERVIEW_COLLECTION") { + const res = await api.get(`/interviews?limit=50`); + const data = res.data?.data || res.data?.interviews || []; + return data; + } + + } catch (error: any) { + console.error("RAG Fetch Error:", error.message); + return questionContext; + } + return null; +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index f1cdd40..13a2bef 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "coc-member", diff --git a/frontend/bun.lock b/frontend/bun.lock index 253fd66..429e613 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "frontend", @@ -22,7 +23,7 @@ "axios": "^1.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "framer-motion": "^12.23.24", + "framer-motion": "^12.25.0", "lexical": "^0.38.2", "lucide-react": "^0.548.0", "motion": "^12.23.24", @@ -568,7 +569,7 @@ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - "framer-motion": ["framer-motion@12.23.24", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w=="], + "framer-motion": ["framer-motion@12.25.0", "", { "dependencies": { "motion-dom": "^12.24.11", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-mlWqd0rApIjeyhTCSNCqPYsUAEhkcUukZxH3ke6KbstBRPcxhEpuIjmiUQvB+1E9xkEm5SpNHBgHCapH/QHTWg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -760,9 +761,9 @@ "motion": ["motion@12.23.24", "", { "dependencies": { "framer-motion": "^12.23.24", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw=="], - "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="], + "motion-dom": ["motion-dom@12.24.11", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A=="], - "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], + "motion-utils": ["motion-utils@12.24.10", "", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -986,6 +987,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "motion/framer-motion": ["framer-motion@12.23.24", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1005,5 +1008,9 @@ "@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "motion/framer-motion/motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="], + + "motion/framer-motion/motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], } } diff --git a/frontend/package.json b/frontend/package.json index b22c562..e3791e8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,7 @@ "axios": "^1.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "framer-motion": "^12.23.24", + "framer-motion": "^12.25.0", "lexical": "^0.38.2", "lucide-react": "^0.548.0", "motion": "^12.23.24", @@ -36,8 +36,8 @@ "react-dom": "^19.1.0", "react-hook-form": "^7.58.1", "react-hot-toast": "^2.6.0", - "react-markdown": "^10.1.0", "react-icons": "^5.5.0", + "react-markdown": "^10.1.0", "react-router-dom": "^7.6.2", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.10" diff --git a/frontend/src/App.css b/frontend/src/App.css index 3fb3548..f07db72 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -40,3 +40,44 @@ .read-the-docs { color: #888; } */ + + +/* LIGHT THEME */ +:root { + /* CHATBOT – LIGHT */ + --color-chatbot-border: #2C1810; + --color-chatbot-bg-window: #FFF6EE; + --color-chatbot-bg-messages: #F5E6D3; + --color-chatbot-text: #2C1810; +} + +.dark { + /* CHATBOT – DARK */ + --color-chatbot-border: #F5E6D3; + --color-chatbot-bg-window: #121212; + --color-chatbot-bg-messages: #1A1A1A; + --color-chatbot-text: #F5E6D3; +} + +.typing-dot { + width: 6px; + height: 6px; + background-color: currentColor; /* 👈 theme ke hisaab se */ + border-radius: 50%; + animation: typing 1.4s infinite ease-in-out; +} + +.typing-dot.delay-1 { + animation-delay: 0.2s; +} +.typing-dot.delay-2 { + animation-delay: 0.4s; +} + +@keyframes typing { + 0% { opacity: 0.3; transform: translateY(0); } + 20% { opacity: 1; transform: translateY(-3px); } + 40% { opacity: 0.3; transform: translateY(0); } +} + + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index fcba206..5dd88ca 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,6 @@ import "./App.css"; -import { Navigate, Route , Routes } from "react-router-dom"; +import { useState } from "react"; +import { Navigate, Route, Routes } from "react-router-dom"; import { Toaster } from "react-hot-toast"; import ThemeToggle from "./components/common/ThemeToggle"; import Home from "./pages/LandingPage"; @@ -10,26 +11,44 @@ import AuthPage from "./pages/Signup"; import NotFound from "./pages/NotFound"; import MainLayout from "./components/miniCompo/MainLayout"; import ProtectedRoute from "./utils/ProtectedRoute"; +import ChatbotWrapper from "./components/chatbot/ChatbotWrapper"; +import { useChatbotVisibility } from './hooks/useChatbotVisibility'; + +const ChatbotContainer = ({ currentQuestionContext }) => { + const isChatbotVisible = useChatbotVisibility(); + + return isChatbotVisible ? ( + + ) : null; +}; + function App() { + const [currentQuestionContext, setCurrentQuestionContext] = useState(null); + return ( <> - } /> - } > - } /> - } /> - } /> - } /> - }> - } /> - + } /> + } > + } /> + } /> + } /> + } /> + }> + } /> - } /> + + } /> + ); } diff --git a/frontend/src/components/chatbot/ChatbotIcon.jsx b/frontend/src/components/chatbot/ChatbotIcon.jsx new file mode 100644 index 0000000..4cbe2e0 --- /dev/null +++ b/frontend/src/components/chatbot/ChatbotIcon.jsx @@ -0,0 +1,96 @@ +"use client"; +import { motion, AnimatePresence, useAnimation } from "framer-motion"; +import { useEffect, useState } from "react"; + +const DARK_BROWN = "#2C1810"; +const BURNT_ORANGE = "#C1502E"; +const OFF_WHITE_LIGHT = "#FFF6EE"; +const SAFFRON = "#FF9933"; + +const CodingCortexIcon = ({ isHovered }) => { + const javaCode = ["public class Cortex {", " void help() {", " System.out.println();", " }", "}"]; + return ( + + + + + + + + {!isHovered ? ( + + + + + + ) : ( + + +
+ + {javaCode.join("\n") + "\n" + javaCode.join("\n")} + +
+
+
+ )} +
+ + + +
+ ); +}; + +export default function ChatbotIcon({ onClick }) { + const [hovered, setHovered] = useState(false); + const controls = useAnimation(); + + useEffect(() => { + if (!hovered) { + controls.start({ y: [0, -8, 0], transition: { duration: 3, repeat: Infinity, ease: 'easeInOut' } }); + } else { + controls.start({ scale: 1.1, transition: { duration: 0.2 } }); + } + }, [hovered, controls]); + + return ( +
+ + {hovered && ( + +
+ System: + system.ask() +
+
+ )} +
+ + setHovered(true)} + onMouseLeave={() => setHovered(false)} + animate={controls} + className="relative p-1 border-4 transition-all duration-300 cursor-pointer" + style={{ + borderColor: DARK_BROWN, + backgroundColor: hovered ? "#FFFFFF" : OFF_WHITE_LIGHT, + borderRadius: "12px", + boxShadow: hovered + ? `8px 8px 0px ${BURNT_ORANGE}, 0px 0px 15px rgba(193, 80, 46, 0.4)` + : `6px 6px 0px ${DARK_BROWN}`, + }} + > +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/chatbot/ChatbotWrapper.jsx b/frontend/src/components/chatbot/ChatbotWrapper.jsx new file mode 100644 index 0000000..8dc85a8 --- /dev/null +++ b/frontend/src/components/chatbot/ChatbotWrapper.jsx @@ -0,0 +1,310 @@ +"use client"; +import { motion, AnimatePresence } from "framer-motion"; +import ReactMarkdown from "react-markdown"; +import { useState, useRef, useEffect } from "react"; +import ChatbotIcon from "./ChatbotIcon"; +import { useLocation } from "react-router-dom"; + + +const COLORS = { + border: "var(--color-chatbot-border, #000000)", + window: "var(--color-chatbot-bg-window, #F5E6D3)", + messages: "var(--color-chatbot-bg-messages, #EAD8C3)", + text: "var(--color-chatbot-text, #2C1810)", + + burntOrange: "#C1502E", + saffron: "#FF9933", +}; + +// --- NEW NEOMORPHIC TERMINAL ICON --- +const TerminalIcon = ({ size = "md" }) => { + const isSm = size === "sm"; + return ( +
+
+ {">"} + _ +
+ {/* Decorative Brackets */} +
+ {"{}"} +
+
+ ); +}; + +export default function ChatbotWrapper({ currentQuestionContext }) { + const [isOpen, setIsOpen] = useState(false); + const [isFull, setIsFull] = useState(false); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [isTyping, setIsTyping] = useState(false); + const location = useLocation(); + + const messagesEndRef = useRef(null); + const lastQuestionContext = useRef(null); + const abortTypingRef = useRef(false); + + + useEffect(() => { + if (currentQuestionContext) { + lastQuestionContext.current = currentQuestionContext; + } + }, [currentQuestionContext]); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + if (!isTyping) { + scrollToBottom(); + } + }, [messages]); + + + + const toggleChat = () => { + setIsOpen((prev) => { + const newState = !prev; + + if (newState && messages.length === 0) { + const welcome = "How can I help you to debug your problem today?"; + let currentText = ""; + let index = 0; + + setTimeout(() => { + const typeWelcome = () => { + if (index < welcome.length) { + currentText += welcome.charAt(index); + setMessages([{ from: "bot", text: currentText }]); + index++; + setTimeout(typeWelcome, 25); + } else { + setMessages([{ from: "bot", text: welcome }]); + } + }; + + typeWelcome(); + }, 500); + } + + return newState; + }); + }; + + + const toggleFullScreen = () => setIsFull((prev) => !prev); + + + const sendMessage = async () => { + if (!input.trim() || isTyping) return; + + const userQuery = input; + const currentPath = location.pathname; // This is the "Context Awareness" part! + + + const updatedMessages = [...messages, { from: "user", text: userQuery }]; + setMessages(updatedMessages); + setInput(""); + setIsTyping(true); + + try { + // 3. Call your BACKEND instead of calling Gemini directly from frontend + const response = await fetch(`${import.meta.env.VITE_API_URL}/chat`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: currentPath, // "/dsa" + userAnswer: userQuery, + questionContext: lastQuestionContext.current, + history: messages, + + }), + }); + + const data = await response.json(); + + setMessages([...updatedMessages, { from: "bot", text: data.reply }]); + setIsTyping(false); + + } catch (error) { + setMessages([...updatedMessages, { from: "bot", text: "ERR_REFUSED: Check your connection." }]); + } finally { + setIsTyping(false); + } + }; + + + const stopStreaming = () => { + abortTypingRef.current = true; + setIsTyping(false); + }; + + + return ( + <> + + + + + + {isOpen && ( + // Wrapper to hold the floating effect +
+ + {/* Neomorphic Deep Shadow Layer */} +
+ + {/* Main Window */} + + + {/* Header */} +
+
+ +
+

Cortex.exe

+

{">"} STATUS: ACTIVE

+
+
+ + {/* Header Buttons */} +
+ + +
+ +
+ + {/* Messages */} +
+ {messages.map((msg, idx) => ( +
+
+ {msg.from !== "user" && } + +
+ {children}, + pre: ({ children }) =>
{children}
+ }} + > + {msg.text} +
+
+
+
+ ))} + + {isTyping && ( +
+ +
+ +
+
+
+
+
+ )} + +
+
+ + {/* Input Area */} +
+
+ {">"} + setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && sendMessage()} + placeholder="ENTER QUERY..." + + className="flex-1 bg-white border-4 border-black p-3 text-xs font-bold uppercase text-black placeholder:text-black/40 focus:outline-none" + style={{ boxShadow: `4px 4px 0 0 ${COLORS.border}` }} + /> + +
+
+ + +
+ )} + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/dsa/questions/QuestionView.jsx b/frontend/src/components/dsa/questions/QuestionView.jsx index a9492ea..976cf95 100644 --- a/frontend/src/components/dsa/questions/QuestionView.jsx +++ b/frontend/src/components/dsa/questions/QuestionView.jsx @@ -1,4 +1,5 @@ import { useQuestions } from "@/hooks/useQuestions"; +import { buildQuestionContext } from "@/utils/buildQuestionContext"; import { ArrowLeft, CheckCircle2, @@ -11,7 +12,7 @@ import { useState, useMemo, useEffect } from "react"; import { SearchBar } from "../../common/SearchBar"; import { EmptyState } from "../../common/EmptyState"; -export function QuestionsView({ selectedTopic, onBack }) { +export function QuestionsView({ selectedTopic, onBack, setCurrentQuestionContext, }) { const { questions, isLoading, toggle, completed } = useQuestions(selectedTopic.id); const [difficultyFilter, setDifficultyFilter] = useState("All"); const [searchTerm, setSearchTerm] = useState(""); @@ -48,7 +49,7 @@ export function QuestionsView({ selectedTopic, onBack }) { }; const difficulties = ["All", "Easy", "Medium", "Hard"]; - + return (
@@ -98,19 +99,17 @@ export function QuestionsView({ selectedTopic, onBack }) {