diff --git a/backend/collaboration-service/consumer.js b/backend/collaboration-service/consumer.js new file mode 100644 index 0000000000..16d9ca83bd --- /dev/null +++ b/backend/collaboration-service/consumer.js @@ -0,0 +1,139 @@ +const { Mistral } = require('@mistralai/mistralai'); + +const amqp = require('amqplib/callback_api'); +const { sendWsMessage, broadcastToRoom } = require('./ws'); +const axios = require('axios'); +const dotenv = require('dotenv'); +dotenv.config(); + +const CLOUDAMQP_URL = process.env.CLOUDAMQP_URL; +const COLLAB_SERVICE_URL = "http://localhost:8003"; + +function arrayEquals(a, b) { + return Array.isArray(a) && + Array.isArray(b) && + a.length === b.length && + a.every((val, index) => val === b[index]); +} + +function checkSubset(parentArray, subsetArray) { + return subsetArray.every((el) => { + return parentArray.includes(el) + }); +} + +// In-memory store to track unmatched users +let unmatchedUsers = []; + +// Function to set up RabbitMQ consumer +const setupConsumer = () => { + amqp.connect(CLOUDAMQP_URL, (err, conn) => { + if (err) { + console.error('Connection error in consumer.js:', err); + return; + } + + conn.createChannel((err, ch) => { + if (err) throw err; + const queue = 'collab_queue'; + ch.assertQueue(queue, { durable: false }); + + console.log('Listening for messages in RabbitMQ queue for collab...'); + ch.consume(queue, async (msg) => { + const userRequest = JSON.parse(msg.content.toString()); + console.log('Received user request:', userRequest); + console.log('User request type:', userRequest.type); + if (userRequest.status === 'cancel') { + // Handle cancel request + const userIndex = unmatchedUsers.findIndex(u => u.userId === userRequest.userId); + if (userIndex !== -1) { + console.log(`Cancelling request for user ${userRequest.userId}`); + clearTimeout(unmatchedUsers[userIndex].timeoutId); // Clear any pending timeout + unmatchedUsers.splice(userIndex, 1); // Remove user from unmatched list + sendWsMessage(userRequest.userId, { status: 'CANCELLED' }); + console.log(`Cancelled matching request for user ${userRequest.userId}`); + } else { + console.log(`No unmatched request found for user ${userRequest.userId}`); + } + sendWsMessage(userRequest.userId, { status: 'CANCELLED' }); + console.log(`Cancelled matching request for user ${userRequest.userId}`); + } else if (userRequest.type === 'ASK_COPILOT') { + // Function to make the API call with retry logic + + try { + const apiKey = process.env.MISTRAL_API_KEY; + const client = new Mistral({ apiKey: apiKey }); + prompt = userRequest.prompt; + currentCode = userRequest.code; + + const chatResponse = await client.chat.complete({ + model: 'mistral-large-latest', + messages: [{role: 'user', content: currentCode + '\n' + prompt}], + }); + console.log('Asking Copilot:', chatResponse); + + broadcastToRoom(userRequest.roomId, { type: 'ASK_COPILOT', response: chatResponse.choices[0].message.content }); + } catch (error) { + console.error("Failed to fetch chat response:", error); + broadcastToRoom(userRequest.roomId, { type: 'ASK_COPILOT', response: "Error fetching response from assistant." }); + } + } + else { + // Handle match request + const match = unmatchedUsers.find(u => + checkSubset(u.category, userRequest.category) || + checkSubset(userRequest.category, u.category) + ) || unmatchedUsers.find(u => u.difficulty === userRequest.difficulty); + + if (match) { + try { + console.log(`Matched user ${userRequest.userId} with user ${match.userId}`); + + // Create room in collaboration service + const response = await axios.post(`${COLLAB_SERVICE_URL}/rooms/create`, { + users: [userRequest.userId, match.userId], + difficulty: userRequest.difficulty, + category: userRequest.category + }); + console.log(response.data); + const { roomId } = response.data; + + // Notify both users + [userRequest, match].forEach(user => { + sendWsMessage(user.userId, { + status: 'MATCH_FOUND', + roomId, + matchedUserId: user === userRequest ? match.userId : userRequest.userId, + difficulty: userRequest.difficulty, + category: userRequest.category + }); + }); + + // Clear the timeouts for both users + clearTimeout(match.timeoutId); + + // Remove matched user from unmatchedUsers + unmatchedUsers = unmatchedUsers.filter(u => u.userId !== match.userId); + } catch (error) { + console.error('Error creating room:', error); + } + } else { + // Set a timeout to remove unmatched users after 30 seconds + const timeoutId = setTimeout(() => { + unmatchedUsers = unmatchedUsers.filter(u => u.userId !== userRequest.userId); + sendWsMessage(userRequest.userId, { status: 'timeout' }); + }, 30000); // 30 seconds timeout + + // Add the new user with their timeout ID + unmatchedUsers.push({ ...userRequest, timeoutId }); + } + } + + ch.ack(msg); // Acknowledge message processing + }); + }); + }); +}; + + +module.exports = { setupConsumer }; diff --git a/backend/collaboration-service/controllers/copilotControllers.js b/backend/collaboration-service/controllers/copilotControllers.js new file mode 100644 index 0000000000..29f862dd8c --- /dev/null +++ b/backend/collaboration-service/controllers/copilotControllers.js @@ -0,0 +1,16 @@ +const { sendToQueue } = require("../../collaboration-service/mq"); + +const askCopilot = async (req, res) => { + + const { code, prompt, type, roomId } = req.body; + console.log(`Received request to ask Copilot for prompt: ${prompt}`); + + sendToQueue({ code, prompt, type, roomId }); + + res.status(200).send({ status: 'Request received. Waiting for Copilot response.' }); + +}; + +module.exports = { + askCopilot, +}; \ No newline at end of file diff --git a/backend/collaboration-service/index.js b/backend/collaboration-service/index.js index 266c9b41d7..0e14454414 100644 --- a/backend/collaboration-service/index.js +++ b/backend/collaboration-service/index.js @@ -1,7 +1,13 @@ +const dotenv = require('dotenv'); +dotenv.config(); const express = require('express'); const cors = require('cors'); const { setupWebSocket } = require('./ws'); const roomRoutes = require('./routes/room'); +const { setupConsumer } = require('./consumer'); +require('dotenv').config(); +const amqp = require('amqplib/callback_api'); + const app = express(); const PORT = process.env.PORT || 8003; @@ -17,3 +23,7 @@ const server = app.listen(PORT, () => { app.use('/rooms', roomRoutes); setupWebSocket(server); + +setupConsumer(); + + diff --git a/backend/collaboration-service/mq.js b/backend/collaboration-service/mq.js new file mode 100644 index 0000000000..886a139075 --- /dev/null +++ b/backend/collaboration-service/mq.js @@ -0,0 +1,33 @@ +const amqp = require('amqplib/callback_api'); +const dotenv = require('dotenv'); +dotenv.config(); + +const CLOUDAMQP_URL = process.env.CLOUDAMQP_URL; + +let channel; + +// Establish connection to RabbitMQ and create a channel +amqp.connect(CLOUDAMQP_URL, (err, conn) => { + if (err) throw err; + + conn.createChannel((err, ch) => { + if (err) throw err; + channel = ch; + const queue = 'collab_queue'; + ch.assertQueue(queue, { durable: false }); + console.log('RabbitMQ connected, queue asserted:', queue); + }); +}); + +// Function to send messages to the queue +const sendToQueue = (message) => { + const queue = 'collab_queue'; + if (!channel) { + console.error('RabbitMQ channel not initialised'); + return; + } + channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); + console.log('Sent message to RabbitMQ for collab:', message); +}; + +module.exports = { sendToQueue }; diff --git a/backend/collaboration-service/package-lock.json b/backend/collaboration-service/package-lock.json index 0fdf945a21..a5a919c15b 100644 --- a/backend/collaboration-service/package-lock.json +++ b/backend/collaboration-service/package-lock.json @@ -9,17 +9,68 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@mistralai/mistralai": "^1.1.0", + "amqplib": "^0.10.4", + "axios": "^1.7.7", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", "http": "^0.0.1-security", "uuid": "^10.0.0", - "ws": "^8.18.0" + "ws": "^8.18.0", + "zod": "^3.23.8" }, "devDependencies": { "nodemon": "^3.1.7" } }, + "node_modules/@acuminous/bitsyntax": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", + "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "debug": "^4.3.4", + "safe-buffer": "~5.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@acuminous/bitsyntax/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@acuminous/bitsyntax/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/@acuminous/bitsyntax/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/@mistralai/mistralai": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.1.0.tgz", + "integrity": "sha512-YueaIX+g4+QTX6ERLjZLZMOhlC0/EoqwpayWrUKfTM9EGTyiOPdxFLpLpg5B9PsaxOrmZDC88pOp4QgSMqVr8w==", + "peerDependencies": { + "zod": ">= 3" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -33,6 +84,20 @@ "node": ">= 0.6" } }, + "node_modules/amqplib": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.4.tgz", + "integrity": "sha512-DMZ4eCEjAVdX1II2TfIUpJhfKAuoCeDIo/YyETbfAqehHTXxxs7WOOd+N1Xxr4cKhx12y23zk8/os98FxlZHrw==", + "dependencies": { + "@acuminous/bitsyntax": "^0.1.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -53,6 +118,21 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -121,6 +201,11 @@ "node": ">=8" } }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -174,6 +259,17 @@ "fsevents": "~2.3.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -217,6 +313,11 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -256,6 +357,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -411,6 +520,38 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -656,6 +797,11 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -882,6 +1028,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -904,6 +1055,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -928,6 +1084,17 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -941,6 +1108,11 @@ "node": ">=8.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1097,6 +1269,11 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1171,6 +1348,15 @@ "node": ">= 0.8" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1222,6 +1408,14 @@ "optional": true } } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/backend/collaboration-service/package.json b/backend/collaboration-service/package.json index 672f42109c..30df885cb8 100644 --- a/backend/collaboration-service/package.json +++ b/backend/collaboration-service/package.json @@ -10,12 +10,16 @@ "license": "ISC", "description": "", "dependencies": { + "@mistralai/mistralai": "^1.1.0", + "amqplib": "^0.10.4", + "axios": "^1.7.7", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", "http": "^0.0.1-security", "uuid": "^10.0.0", - "ws": "^8.18.0" + "ws": "^8.18.0", + "zod": "^3.23.8" }, "devDependencies": { "nodemon": "^3.1.7" diff --git a/backend/collaboration-service/routes/room.js b/backend/collaboration-service/routes/room.js index 2674b511b5..e3a2ea40d6 100644 --- a/backend/collaboration-service/routes/room.js +++ b/backend/collaboration-service/routes/room.js @@ -7,6 +7,12 @@ const { leaveRoom } = require('../controllers/roomControllers'); +const { + askCopilot +} = require('../controllers/copilotControllers'); + +router.post('/', askCopilot); + router.post('/create', createRoom); router.get('/:roomId', getRoomInfo); router.post('/:roomId/join', joinRoom); diff --git a/backend/collaboration-service/ws.js b/backend/collaboration-service/ws.js index d2c3de7a77..56755c5cd0 100644 --- a/backend/collaboration-service/ws.js +++ b/backend/collaboration-service/ws.js @@ -290,6 +290,18 @@ function broadcastToRoom(roomId, message, excludeUserId = null) { } } +// Helper function to send a message to a specific user by userId +const sendWsMessage = (userId, message) => { + const ws = wsClients.get(userId); + if (ws) { + ws.send(JSON.stringify(message)); + console.log(`Sent WebSocket message to user ${userId}:`, message); + } else { + console.log(`No WebSocket connection found for user ${userId}`); + } +}; + + module.exports = { - setupWebSocket, + setupWebSocket, sendWsMessage, broadcastToRoom }; diff --git a/backend/matching-service/consumer.js b/backend/matching-service/consumer.js index 1e50e2fd0e..93925c998d 100644 --- a/backend/matching-service/consumer.js +++ b/backend/matching-service/consumer.js @@ -1,3 +1,6 @@ +const { Mistral } = require('@mistralai/mistralai'); + + const amqp = require('amqplib/callback_api'); const { sendWsMessage } = require('./ws'); const axios = require('axios'); @@ -52,6 +55,29 @@ const setupConsumer = () => { } sendWsMessage(userRequest.userId, { status: 'CANCELLED' }); console.log(`Cancelled matching request for user ${userRequest.userId}`); + } else if (userRequest.status === 'askcopilot') { + // Handle askcopilot request: Call LLM API with the data + const apiKey = process.env.Mistral_API_KEY; + const client = new Mistral ({apiKey: apiKey}); + const prompt = userRequest.data.prompt; + const code = userRequest.data.code; + model = 'mistral-large-latest' + chat_response = await client.chat.complete( + + model=model, + messages=[ + { + "role": "system", + "content": "You are an experienced developer. Please provide detailed and accurate responses." + }, + { + "role": "user", + "content": "Prompt: ${prompt}\nCode: ${code}" + } + ] + ) + + sendWsMessage(userRequest.userId, { status: 'askcopilot', response: chat_response }); } else { // Handle match request const match = unmatchedUsers.find(u => diff --git a/frontend/src/api/CopilotApi.js b/frontend/src/api/CopilotApi.js new file mode 100644 index 0000000000..c7cce564c4 --- /dev/null +++ b/frontend/src/api/CopilotApi.js @@ -0,0 +1,21 @@ +import axios from 'axios'; + +const API_URL = 'http://localhost:8003/rooms'; + +export const askCopilot = async (data) => { + try { + const response = await axios.post(API_URL, data); + if (response.status === 200) { + return response.data; + } else { + throw new Error('No match found.'); + } + } catch (error) { + if (error.response) { + console.error('Error finding match:', error.response.data); + throw new Error(error.response.data.message); + } + console.error('Error finding match:', error); + throw error; // Re-throw the error to handle it in component + } +} \ No newline at end of file diff --git a/frontend/src/pages/student/CollaborationRoom.js b/frontend/src/pages/student/CollaborationRoom.js index 0b0aafcc05..2f8a1154da 100644 --- a/frontend/src/pages/student/CollaborationRoom.js +++ b/frontend/src/pages/student/CollaborationRoom.js @@ -1,6 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; import Editor from "@monaco-editor/react"; import { useParams, useLocation } from "react-router-dom"; +import { askCopilot } from "../../api/CopilotApi"; const languages = [ { label: "JavaScript", value: "javascript" }, @@ -27,7 +28,10 @@ const CollaborationRoom = () => { const [ws, setWs] = useState(null); // Manage the WebSocket connection here. const [code, setCode] = useState("// Start coding..."); const [language, setLanguage] = useState("javascript"); - + + const [userPrompt, setUserPrompt] = useState(""); // Track the user input for the prompt + const [copilotResponse, setCopilotResponse] = useState(""); // Store the response from Copilot API + const monacoRef = useRef(null); // Store reference to Monaco instance const editorRef = useRef(null); // Store reference to Monaco Editor instance @@ -82,6 +86,8 @@ const CollaborationRoom = () => { setStatus(`Failed to create room: ${result.message}`); } else if (result.type === "LANGUAGE_CHANGE") { setLanguage(result.language); + } else if (result.type === "ASK_COPILOT") { + setCopilotResponse(result.response); } }; @@ -192,6 +198,24 @@ const CollaborationRoom = () => { } }; + const handleSubmitPrompt = async () => { + const promptData = { + code: code, + prompt: userPrompt, + type: "ASK_COPILOT", + roomId: roomId, + }; + + try { + const response = await askCopilot(promptData); + + } catch (error) { + console.error("Error calling Copilot API:", error); + setCopilotResponse("Error: " + error); + } + }; + + console.log("Message:", message); console.log("Messages:", messages); @@ -247,6 +271,20 @@ const CollaborationRoom = () => { > +
+