diff --git a/.gitignore b/.gitignore index 3c3629e..1dcef2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.env \ No newline at end of file diff --git a/backend/index.js b/backend/index.js index f421bb8..d50e9a4 100644 --- a/backend/index.js +++ b/backend/index.js @@ -100,6 +100,6 @@ app.get("*", (req, res) => { res.sendFile(path.join(__dirname, "../frontend/vite-project/dist/index.html")); }); -server.listen(port, () => { - console.log(`Server is running on port ${port}`); -}); +// server.listen(port, () => { +// console.log(`Server is running on port ${port}`); +// }); diff --git a/backend/routes/httpRoutes.js b/backend/routes/httpRoutes.js index f6419bb..8de052f 100644 --- a/backend/routes/httpRoutes.js +++ b/backend/routes/httpRoutes.js @@ -2,11 +2,12 @@ import express from 'express'; import RoomController from '../controllers/RoomController.js'; import VideoCallController from '../controllers/VideoCallController.js'; +import { GeminiAiService } from '../services/geminiAiService.js'; // Create controller instances (io will be null for HTTP routes) const roomController = new RoomController(null); const videoCallController = new VideoCallController(null); - +const geminiService = new GeminiAiService(); const router = express.Router(); // Health check endpoint @@ -64,6 +65,24 @@ router.get('/api/rooms', (req, res) => { } }); + +router.post("/api/chatai", async (req, res) => { + try { + const { message, history = [] } = req.body; + if (!message) { + return res.status(400).json({ error: "Message is required" }); + } + const resp = await geminiService.chatWithGemini(message, history); + return res.json({ data: resp.data }); + } catch (err) { + console.error("[chatBotRoute] error:", err.message || err); + return res.status(500).json({ + error: "Failed to fetch response from Gemini AI", + details: (err.message || "").slice(0, 1000), + }); + } +}); + // API documentation endpoint router.get('/api/docs', (req, res) => { res.json({ diff --git a/backend/server.js b/backend/server.js index 0a758b6..47a8c8d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7,7 +7,8 @@ import { Server } from "socket.io"; import path, { dirname, join } from "path"; import { fileURLToPath } from "url"; import cors from "cors"; - +import dotenv from "dotenv" +dotenv.config({ path: './.env' }); // Import configurations import { serverConfig, staticConfig } from './config/server.js'; diff --git a/backend/services/geminiAiService.js b/backend/services/geminiAiService.js new file mode 100644 index 0000000..56fc3a0 --- /dev/null +++ b/backend/services/geminiAiService.js @@ -0,0 +1,61 @@ +import axios from "axios"; + +const GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"; +const API_KEY= YOUR_API_KEY; + +if (!API_KEY) { + throw new Error("GEMINI_API_KEY is not set in environment variables"); +} + +function extractReply(resp) { + try { + return ( + resp.data?.candidates?.[0]?.content?.parts?.[0]?.text || + resp.data?.candidates?.[0]?.content?.[0]?.text || + resp.data?.output?.[0]?.content?.[0]?.text || + null + ); + } catch (error) { + console.error("Error extracting reply:", error); + return null; + } +} + +export class GeminiAiService { + constructor() { + this.axios = axios.create({ timeout: 30000 }); + } +async chatWithGemini(message, history = []) { + const promptText = `You are an AI coding assistant in a collaborative coding room. Keep responses concise (max 4 lines). User message: ${message}`; + + // 🔥 Convert history into Gemini's format + const formattedHistory = history.map(h => ({ + role: h.role, + parts: [{ text: h.text }] + })); + + const payload = { + contents: [ + ...formattedHistory, + { role: "user", parts: [{ text: promptText }] } + ], + generationConfig: { temperature: 0.3, maxOutputTokens: 512 }, + }; + + try { + const resp = await this.axios.post( + `${GEMINI_API_URL}?key=${API_KEY}`, + payload, + { headers: { "Content-Type": "application/json" } } + ); + + const reply = extractReply(resp); + const tokensUsed = resp.data?.usageMetadata?.totalTokenCount ?? null; + + return { data: { reply: reply || "No reply from Gemini.", tokensUsed } }; + } catch (err) { + console.error("Gemini API Error:", err.response?.status, err.response?.data || err.message); + throw new Error(err.response?.data?.error?.message || err.message || "Gemini request failed"); + } +} +} \ No newline at end of file diff --git a/frontend/vite-project/src/App.jsx b/frontend/vite-project/src/App.jsx index 2dcf5a0..5d4d346 100644 --- a/frontend/vite-project/src/App.jsx +++ b/frontend/vite-project/src/App.jsx @@ -25,6 +25,7 @@ import { import BackToTop from "./components/ui/BackToTop"; import * as monaco from 'monaco-editor'; +import FloatingChatbot from "./components/floating"; const App = () => { @@ -1022,6 +1023,7 @@ const App = () => { )} +
{/* Changed from defaultLanguage to language */} diff --git a/frontend/vite-project/src/components/chatBot.jsx b/frontend/vite-project/src/components/chatBot.jsx new file mode 100644 index 0000000..5bd92e4 --- /dev/null +++ b/frontend/vite-project/src/components/chatBot.jsx @@ -0,0 +1,87 @@ +import React, { useState } from "react"; + +const Chatbot = () => { + const [message, setMessage] = useState(""); + const [history, setHistory] = useState([]); + const [responses, setResponses] = useState([]); + + const API_BASE_URL = + import.meta.env.VITE_API_BASE_URL || "http://localhost:3000"; + + const handleSendMessage = async () => { + if (!message) return; + + const newHistory = [...history, { role: "user", text: message }]; + setHistory(newHistory); + setResponses((prev) => [...prev, { role: "user", text: message }]); + setMessage(""); + + try { + const response = await fetch(`${API_BASE_URL}/api/chatai`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ message, history: newHistory }), + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + const botReply = data.data.reply; + + setResponses((prev) => [...prev, { role: "bot", text: botReply }]); + } catch (error) { + console.error("Error sending message:", error); + } + }; + + return ( +
+ {/* Chat window */} +
+ {responses.map((resp, index) => ( +
+
+ {resp.text} +
+
+ ))} +
+ + {/* Input box */} +
+
+ setMessage(e.target.value)} + placeholder="Type your message..." + className="flex-1 border text-black rounded-xl px-3 py-2 outline-none focus:ring-2 focus:ring-blue-400" + /> + +
+
+
+ ); +}; + +export default Chatbot; \ No newline at end of file diff --git a/frontend/vite-project/src/components/floating.css b/frontend/vite-project/src/components/floating.css new file mode 100644 index 0000000..6be88c0 --- /dev/null +++ b/frontend/vite-project/src/components/floating.css @@ -0,0 +1,82 @@ +/* Floating Button */ +.floating-button { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + color: white; + border-radius: 9999px; + cursor: pointer; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25); + width: 60px; + height: 60px; + transition: all 0.5s ease; + background: linear-gradient(to right, #ec4899, #8b5cf6, #3b82f6); /* pink → purple → blue */ + z-index: 9999; +} + +.floating-button:hover { + background: linear-gradient(to right, #3b82f6, #8b5cf6, #ec4899); + box-shadow: 0px 6px 16px rgba(139, 92, 246, 0.7); +} + +/* Hover Tooltip */ +.tooltip { + position: absolute; + bottom: 70px; + right: 50%; + transform: translateX(50%); + background-color: #ec4899; + color: white; + font-size: 12px; + padding: 6px 12px; + border-radius: 8px; + white-space: nowrap; + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25); +} + +/* Popup Chatbot */ +.chatbot-container { + position: fixed; + bottom: 80px; + right: 20px; + width: 320px; + height: 384px; + background: white; + box-shadow: 0px 6px 16px rgba(0, 0, 0, 0.2); + border-radius: 16px; + display: flex; + flex-direction: column; + z-index: 9998; +} + +/* Header */ +.chatbot-header { + background-color: #9742d0; /* blue-700 */ + color: white; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + border-top-left-radius: 17px; + border-top-right-radius: 16px; + font-size: 17px; + font-weight: 1500; +} + +/* Close Button */ +.close-button { + display: flex; + align-items: center; + justify-content: center; + background: #ef4444; /* red-500 */ + width: 24px; + height: 24px; + border-radius: 6px; + cursor: pointer; + transition: background 0.3s ease; +} + +.close-button:hover { + background: #f87171; /* red-400 */ +} \ No newline at end of file diff --git a/frontend/vite-project/src/components/floating.jsx b/frontend/vite-project/src/components/floating.jsx new file mode 100644 index 0000000..a23529e --- /dev/null +++ b/frontend/vite-project/src/components/floating.jsx @@ -0,0 +1,66 @@ +import React, { useState } from "react"; +import Chatbot from "./chatBot"; +import { MessageCircle, X } from "lucide-react"; +import "./floating.css" + +const FloatingChatbot = () => { + const [open, setOpen] = useState(false); + const [position, setPosition] = useState({ x: 20, y: 20 }); + const [hover, setHover] = useState(false); + + const handleDrag = (e) => { + const screenW = window.innerWidth; + const screenH = window.innerHeight; + + let newX = screenW - e.clientX - 50; + let newY = screenH - e.clientY - 50; + + if (newX < 10) newX = 10; + if (newY < 10) newY = 10; + + setPosition({ x: newX, y: newY }); + }; + + return ( + <> + {/* Floating Icon */} +
setOpen(!open)} + onDrag={(e) => handleDrag(e)} + draggable + onMouseEnter={() => setHover(true)} + onMouseLeave={() => setHover(false)} + className="floating-button" + style={{ + bottom: position.y, + right: position.x, + }} + > + + + {/* Hover Text */} + {hover &&
Chatbot
} +
+ + {/* Popup Chatbot */} + {open && ( +
+ {/* Header */} +
+ AI Chatbot + +
+ + {/* Chatbot Component */} +
+ +
+
+ )} + + ); +}; + +export default FloatingChatbot; \ No newline at end of file