Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
.env
6 changes: 3 additions & 3 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
// });
21 changes: 20 additions & 1 deletion backend/routes/httpRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand Down
3 changes: 2 additions & 1 deletion backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
61 changes: 61 additions & 0 deletions backend/services/geminiAiService.js
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
2 changes: 2 additions & 0 deletions frontend/vite-project/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import BackToTop from "./components/ui/BackToTop";

import * as monaco from 'monaco-editor';
import FloatingChatbot from "./components/floating";


const App = () => {
Expand Down Expand Up @@ -1022,6 +1023,7 @@ const App = () => {
)}
</span>
</div>
<FloatingChatbot/>
<div style={{ position: "relative", height: "calc(100% - 40px)" }}>

{/* Changed from defaultLanguage to language */}
Expand Down
87 changes: 87 additions & 0 deletions frontend/vite-project/src/components/chatBot.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col h-full">
{/* Chat window */}
<div className="flex-1 overflow-y-auto p-3 space-y-3 bg-gray-50">
{responses.map((resp, index) => (
<div
key={index}
className={`flex ${
resp.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`px-3 py-2 rounded-xl max-w-[75%] ${
resp.role === "user"
? "bg-purple-500 text-white rounded-br-none"
: "bg-gray-200 text-gray-800 rounded-bl-none"
}`}
>
{resp.text}
</div>
</div>
))}
</div>

{/* Input box */}
<div>
<div className="flex-1 items-center border-t p-2 gap-2">
<input
type="text"
value={message}
onChange={(e) => 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"
/>
<button
onClick={handleSendMessage}
className="bg-purple-600 hover:bg-purple-400 text-white px-4 py-2 rounded-xl transition"
>
Send
</button>
</div>
</div>
</div>
);
};

export default Chatbot;
82 changes: 82 additions & 0 deletions frontend/vite-project/src/components/floating.css
Original file line number Diff line number Diff line change
@@ -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 */
}
66 changes: 66 additions & 0 deletions frontend/vite-project/src/components/floating.jsx
Original file line number Diff line number Diff line change
@@ -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 */}
<div
onClick={() => setOpen(!open)}
onDrag={(e) => handleDrag(e)}
draggable
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
className="floating-button"
style={{
bottom: position.y,
right: position.x,
}}
>
<MessageCircle size={28} />

{/* Hover Text */}
{hover && <div className="tooltip">Chatbot</div>}
</div>

{/* Popup Chatbot */}
{open && (
<div className="chatbot-container">
{/* Header */}
<div className="chatbot-header">
<span>AI Chatbot</span>
<button onClick={() => setOpen(false)} className="close-button">
<X size={14} color="white" />
</button>
</div>

{/* Chatbot Component */}
<div className="flex-1">
<Chatbot />
</div>
</div>
)}
</>
);
};

export default FloatingChatbot;