diff --git a/backend/controllers/assistant.controller.js b/backend/controllers/assistant.controller.js new file mode 100644 index 0000000..559e8f9 --- /dev/null +++ b/backend/controllers/assistant.controller.js @@ -0,0 +1,86 @@ +const User = require('../models/User'); + +// Placeholder for future LLM integration +// You can replace this function with a call to OpenAI/Gemini, etc. +async function generateAssistantReply(prompt, context) { + // Simple rules-based responses using available context until LLM is integrated + const lines = []; + if (context?.streak != null) { + lines.push(`Your current streak is ${context.streak} day${context.streak === 1 ? '' : 's'}.`); + if (context.streak >= 7) { + lines.push('🔥 Great job keeping a 7+ day streak! Keep it going!'); + } + } + if (Array.isArray(context?.goals) && context.goals.length) { + lines.push(`You have ${context.goals.length} active goal${context.goals.length === 1 ? '' : 's'}.`); + } + if (Array.isArray(context?.activity)) { + const last7 = context.activity.slice(-7); + lines.push(`Last 7 entries recorded: ${last7.length}.`); + } + if (context?.timeSpent) { + lines.push(`Time spent: ${context.timeSpent}.`); + } + if (lines.length === 0) { + lines.push('I can summarize your DevSync activity once you start logging!'); + } + lines.push('\nTip: Ask me things like "What is my current streak?" or "Summarize my week."'); + return lines.join(' '); +} + +exports.postAssistantMessage = async (req, res) => { + try { + const { message } = req.body; + if (!message || typeof message !== 'string') { + return res.status(400).json({ errors: [{ msg: 'Message is required' }] }); + } + + // Load user activity context + const user = await User.findById(req.user.id).select('-password'); + if (!user) { + return res.status(404).json({ errors: [{ msg: 'User not found' }] }); + } + + const context = { + streak: user.streak, + timeSpent: user.timeSpent, + activity: user.activity, + goals: user.goals, + // Extend with commits/problemsSolved when available + }; + + const reply = await generateAssistantReply(message, context); + + return res.json({ + reply, + insights: { + streak: user.streak, + timeSpent: user.timeSpent, + goalsCount: Array.isArray(user.goals) ? user.goals.length : 0, + recentActivityCount: Array.isArray(user.activity) ? user.activity.slice(-7).length : 0 + } + }); + } catch (err) { + console.error('Assistant error:', err.message); + return res.status(500).json({ errors: [{ msg: 'Server Error' }] }); + } +}; + +// GET health/check + sample nudge +exports.getAssistantNudge = async (req, res) => { + try { + const user = await User.findById(req.user.id).select('-password'); + if (!user) { + return res.status(404).json({ errors: [{ msg: 'User not found' }] }); + } + const msg = user.streak >= 1 + ? `🔥 You maintained a ${user.streak}-day streak, keep it going!` + : 'Let’s kick off your first activity today!'; + res.json({ message: msg }); + } catch (err) { + console.error('Assistant nudge error:', err.message); + res.status(500).json({ errors: [{ msg: 'Server Error' }] }); + } +}; + + diff --git a/backend/package-lock.json b/backend/package-lock.json index e772e60..27142ab 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1024,6 +1024,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index fe1156d..9e1f20d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,4 +32,4 @@ "devDependencies": { "nodemon": "^3.1.7" } -} +} \ No newline at end of file diff --git a/backend/routes/assistant.route.js b/backend/routes/assistant.route.js new file mode 100644 index 0000000..4746900 --- /dev/null +++ b/backend/routes/assistant.route.js @@ -0,0 +1,14 @@ +const express = require('express'); +const router = express.Router(); +const auth = require('../middleware/auth'); +const { postAssistantMessage, getAssistantNudge } = require('../controllers/assistant.controller'); + +// POST /api/assistant/message -> returns assistant reply +router.post('/message', auth, postAssistantMessage); + +// GET /api/assistant/nudge -> small tip/motivation +router.get('/nudge', auth, getAssistantNudge); + +module.exports = router; + + diff --git a/backend/services/emailService.js b/backend/services/emailService.js index 03d9636..a1144eb 100644 --- a/backend/services/emailService.js +++ b/backend/services/emailService.js @@ -10,6 +10,18 @@ const transporter = nodemailer.createTransport({ // Verification email const sendVerificationEmail = async (email, verificationCode) => { + + //Email Verifer GET Request being sent + const {data} = await axios.get(`https://api.hunter.io/v2/email-verifier?email=${email}&api_key=${process.env.EMAIL_VERIFIER_API_KEY}`); + //Verfying the response type + if(data.data.status === 'invalid') + { + //Invalid Email hence Sending a Error Message + console.log('Invalid Email ID') + //Sending the Invalid Email Id Error + throw new Error('Invalid Email ID'); + } + const mailOptions = { from: process.env.EMAIL_USER, to: email, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b83744d..e77e6a6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8163,4 +8163,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 37cb7c9..f04a4e0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -30,6 +30,7 @@ import FloatingSupportButton from "./Components/ui/Support"; function Home() { const [showTop, setShowTop] = useState(false); + const [assistantOpen, setAssistantOpen] = useState(false); useEffect(() => { const handleScroll = () => { diff --git a/frontend/src/Components/ui/ChatAssistant.jsx b/frontend/src/Components/ui/ChatAssistant.jsx new file mode 100644 index 0000000..534f4e9 --- /dev/null +++ b/frontend/src/Components/ui/ChatAssistant.jsx @@ -0,0 +1,97 @@ +import React, { useEffect, useRef, useState } from 'react'; + +const ChatAssistant = ({ open, onClose }) => { + const [messages, setMessages] = useState([ + { role: 'assistant', content: 'Hi! I can summarize your DevSync activity and nudge your productivity. Ask me anything.' } + ]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const scrollRef = useRef(null); + + useEffect(() => { + if (open) { + // Optionally fetch a nudge + const token = localStorage.getItem('token'); + if (!token) return; + fetch(`${import.meta.env.VITE_API_URL}/api/assistant/nudge`, { + headers: { 'x-auth-token': token } + }).then(async (res) => { + if (!res.ok) return; + const data = await res.json(); + setMessages((prev) => [...prev, { role: 'assistant', content: data.message }]); + }).catch(() => {}); + } + }, [open]); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages, open]); + + const send = async () => { + const text = input.trim(); + if (!text) return; + setInput(''); + setMessages((prev) => [...prev, { role: 'user', content: text }]); + + const token = localStorage.getItem('token'); + if (!token) { + setMessages((prev) => [...prev, { role: 'assistant', content: 'Please log in to use the assistant.' }]); + return; + } + + setLoading(true); + try { + const res = await fetch(`${import.meta.env.VITE_API_URL}/api/assistant/message`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': token + }, + body: JSON.stringify({ message: text }) + }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.errors?.[0]?.msg || 'Request failed'); + setMessages((prev) => [...prev, { role: 'assistant', content: data.reply }]); + } catch (e) { + setMessages((prev) => [...prev, { role: 'assistant', content: e.message || 'Something went wrong.' }]); + } finally { + setLoading(false); + } + }; + + if (!open) return null; + + return ( +