Skip to content

Commit 5bbed89

Browse files
committed
Merge remote-tracking branch 'origin/integration-branch' into github-actions
2 parents 91aa185 + 55ede88 commit 5bbed89

File tree

28 files changed

+4088
-304
lines changed

28 files changed

+4088
-304
lines changed

collaboration-service/collaboration-service/env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ GOOGLE_AI_API_KEY=your_google_ai_api_key_here
1010
# External Services
1111
QUESTION_SERVICE_BASE_URL=http://localhost:5050
1212
USER_SERVICE_BASE_URL=http://localhost:3001
13+
MATCHING_SERVICE_URL=http://localhost:4001
1314

1415
# CORS
1516
CORS_ORIGIN=http://localhost:3000,http://localhost:3002,http://localhost:4000

collaboration-service/collaboration-service/src/controllers/aiController.js

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,20 +93,101 @@ export const chatWithAI = async (req, res) => {
9393
export const analyzeSessionCode = async (req, res) => {
9494
try {
9595
const sessionId = req.params.id;
96+
console.log(`[AI Controller] analyzeSessionCode called for session ${sessionId}`);
9697

9798
// Verify session exists
9899
const session = await redisRepo.getJson(`collab:session:${sessionId}`);
99100
if (!session) {
101+
console.error(`[AI Controller] Session ${sessionId} not found in Redis`);
100102
return res.status(404).json({ error: "Session not found" });
101103
}
102104

103-
// Get current code
104-
const doc = await redisRepo.getJson(`collab:document:${sessionId}`);
105-
const code = doc?.text || req.body.code || "";
106-
const language = req.body.language || "python";
105+
// Get current code - try multiple sources
106+
let doc = await redisRepo.getJson(`collab:document:${sessionId}`);
107+
console.log(`[AI Controller] Document from Redis:`, doc ? 'exists' : 'null');
108+
let code = doc?.text || req.body.code || "";
109+
110+
// Get current language from request body or Redis storage
111+
let language = req.body.language;
112+
if (!language) {
113+
// Try to get language from Redis storage
114+
const langData = await redisRepo.getJson(`collab:language:${sessionId}`);
115+
language = langData?.language || "python";
116+
console.log(`[AI Controller] Got language from Redis storage: ${language}`);
117+
} else {
118+
language = language;
119+
}
120+
121+
console.log(`[AI Controller] Code length from Redis: ${code.length}`);
122+
console.log(`[AI Controller] Language for analysis: ${language}`);
123+
124+
// If no code from Redis, try to get from YJS in-memory storage
125+
if (!code || code.trim() === "") {
126+
console.log(`[AI Controller] No code in Redis, trying YJS storage...`);
127+
try {
128+
const { rooms } = await import("../ws/yjsGateway.js");
129+
console.log(`[AI Controller] YJS rooms Map size: ${rooms.size}`);
130+
console.log(`[AI Controller] Looking for room with sessionId: ${sessionId}`);
131+
const room = rooms.get(sessionId);
132+
133+
if (room && room.doc) {
134+
console.log(`[AI Controller] Room found, getting text from YJS...`);
135+
136+
// Try multiple field names (code is what CodePane uses)
137+
const possibleFields = ['code', 'monaco', 'text', 'content', 'doc'];
138+
let foundCode = '';
139+
140+
for (const field of possibleFields) {
141+
try {
142+
const ytext = room.doc.getText(field);
143+
const text = ytext.toString();
144+
console.log(`[AI Controller] YJS field '${field}' has length: ${text.length}`);
145+
if (text.trim() !== '') {
146+
foundCode = text;
147+
console.log(`[AI Controller] Found code in field '${field}'`);
148+
break;
149+
}
150+
} catch (e) {
151+
// Field doesn't exist, try next
152+
}
153+
}
154+
155+
// Also try to get the document state
156+
try {
157+
const state = Y.encodeStateAsUpdate(room.doc);
158+
console.log(`[AI Controller] YJS document state size: ${state.length} bytes`);
159+
160+
// Try to get all text fields
161+
console.log(`[AI Controller] Inspecting YJS document structure...`);
162+
} catch (e) {
163+
console.error(`[AI Controller] Error inspecting document:`, e);
164+
}
165+
166+
code = foundCode;
167+
console.log(`[AI Controller] Final code from YJS storage (length: ${code.length})`);
168+
169+
// Save it to Redis for future access
170+
if (code && code.trim() !== '') {
171+
console.log(`[AI Controller] Saving code to Redis...`);
172+
await redisRepo.setJson(`collab:document:${sessionId}`, {
173+
text: code,
174+
version: Date.now()
175+
});
176+
console.log(`[AI Controller] Saved to Redis successfully`);
177+
} else {
178+
console.log(`[AI Controller] No code found in YJS document`);
179+
}
180+
} else {
181+
console.log(`[AI Controller] No room found in YJS storage for session ${sessionId}`);
182+
console.log(`[AI Controller] Available sessions in YJS:`, Array.from(rooms.keys()));
183+
}
184+
} catch (err) {
185+
console.error("[AI Controller] Could not access YJS storage:", err);
186+
}
187+
}
107188

108-
if (!code) {
109-
return res.status(400).json({ error: "No code to analyze" });
189+
if (!code || code.trim() === "") {
190+
return res.status(400).json({ error: "No code to analyze. Please write some code in the editor first." });
110191
}
111192

112193
// Analyze code

collaboration-service/collaboration-service/src/controllers/sessionController.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,36 @@ export const leaveSession = async (req, res) => {
121121
delete pres[userId];
122122
await redisRepo.setJson(`collab:presence:${s.id}`, pres);
123123

124+
// ✅ Remove user from participants set
125+
await redisRepo.sRem(`collab:session:${s.id}:participants`, userId);
126+
console.log(`[leaveSession] Removed ${userId} from participants set`);
127+
128+
// ✅ Clean up matching service state
129+
try {
130+
const matchingServiceUrl = process.env.NEXT_PUBLIC_MATCHING_SERVICE_URL || "http://localhost:4001";
131+
const response = await axios.post(`${matchingServiceUrl}/match/leave`, { userId });
132+
console.log(`[leaveSession] Matching service cleanup for ${userId}:`, response.status);
133+
} catch (err) {
134+
console.warn(`[leaveSession] Failed to clean up matching service for ${userId}:`, err.message);
135+
}
136+
124137
let status = "active";
125138
if (Object.keys(pres).length === 0) {
126139
s.status = "ended";
127140
await redisRepo.setJson(`collab:session:${s.id}`, s);
128141
status = "ended";
142+
143+
// Clean up both users from matching service when session ends completely
144+
try {
145+
const matchingServiceUrl = process.env.NEXT_PUBLIC_MATCHING_SERVICE_URL || "http://localhost:4001";
146+
const users = [s.userA, s.userB].filter(Boolean);
147+
for (const pid of users) {
148+
const response = await axios.post(`${matchingServiceUrl}/match/leave`, { userId: pid });
149+
console.log(`[leaveSession] Matching service cleanup for ${pid}:`, response.status);
150+
}
151+
} catch (err) {
152+
console.warn(`[leaveSession] Failed to clean up all users from matching service:`, err.message);
153+
}
129154
}
130155

131156
// Broadcast session:end to all remaining participants

collaboration-service/collaboration-service/src/repos/redisRepo.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,9 @@ export const redisRepo = {
8080
const res = await redisClient.sIsMember(key, member);
8181
return res === 1 || res === true;
8282
},
83+
84+
async sRem(key, ...members) {
85+
// node-redis v4 expects a flat array, not nested
86+
await redisClient.sRem(key, members);
87+
},
8388
};

collaboration-service/collaboration-service/src/services/aiService.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import dotenv from "dotenv";
44
dotenv.config();
55

66
// Initialize Gemini AI
7-
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
7+
const apiKey = process.env.GOOGLE_AI_API_KEY || "";
8+
9+
if (!apiKey) {
10+
console.warn("[AI Service] Warning: GOOGLE_AI_API_KEY not found. AI features will not work.");
11+
}
12+
13+
const genAI = new GoogleGenerativeAI(apiKey);
814

915
// Conversation history per session (in-memory for now, can be moved to Redis later)
1016
const sessionConversations = new Map();
@@ -28,6 +34,11 @@ function getConversationHistory(sessionId) {
2834
*/
2935
export async function generateAIResponse(sessionId, userMessage, context = {}) {
3036
try {
37+
// Check if API key is configured
38+
if (!apiKey) {
39+
throw new Error("AI service is not configured. Please set GOOGLE_AI_API_KEY in your environment variables.");
40+
}
41+
3142
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
3243

3344
// Build system context
@@ -88,7 +99,17 @@ IMPORTANT GUIDELINES:
8899

89100
} catch (error) {
90101
console.error("[AI Service] Error generating response:", error);
91-
throw new Error("Failed to generate AI response: " + error.message);
102+
103+
// Provide more helpful error messages
104+
if (error.message.includes("API_KEY")) {
105+
throw new Error("AI service API key is invalid. Please check your configuration.");
106+
} else if (error.message.includes("quota") || error.message.includes("429")) {
107+
throw new Error("AI service quota exceeded. Please try again later.");
108+
} else if (error.message.includes("configuration")) {
109+
throw error; // Re-throw configuration errors as-is
110+
} else {
111+
throw new Error("Failed to generate AI response: " + error.message);
112+
}
92113
}
93114
}
94115

@@ -99,11 +120,19 @@ IMPORTANT GUIDELINES:
99120
* @param {string} language - Programming language
100121
* @returns {Promise<object>} Analysis result
101122
*/
102-
export async function analyzeCode(sessionId, code, language) {
123+
export async function analyzeCode(sessionId, code, language = "python") {
103124
try {
125+
if (!apiKey) {
126+
throw new Error("AI service is not configured");
127+
}
128+
104129
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
105130

106-
const prompt = `Analyze the following ${language} code and provide:
131+
console.log(`[AI Service] Analyzing code in language: ${language}`);
132+
133+
const prompt = `You are analyzing ${language.toUpperCase()} code. The code provided is definitely ${language} code.
134+
135+
Analyze the following ${language} code and provide:
107136
1. Code quality assessment (1-10)
108137
2. Time complexity
109138
3. Space complexity
@@ -146,6 +175,10 @@ Please provide your analysis in a structured, concise format.`;
146175
*/
147176
export async function getHint(sessionId, question, currentCode = "") {
148177
try {
178+
if (!apiKey) {
179+
throw new Error("AI service is not configured");
180+
}
181+
149182
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
150183

151184
let prompt = `Provide a helpful hint (not the complete solution) for this coding problem:
@@ -181,6 +214,10 @@ DIFFICULTY: ${question.difficulty}
181214
*/
182215
export async function debugError(sessionId, code, error, language) {
183216
try {
217+
if (!apiKey) {
218+
throw new Error("AI service is not configured");
219+
}
220+
184221
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
185222

186223
const prompt = `Help debug this ${language} code that's producing an error:
@@ -225,6 +262,10 @@ export function clearConversationHistory(sessionId) {
225262
*/
226263
export async function explainConcept(concept, language = "general") {
227264
try {
265+
if (!apiKey) {
266+
throw new Error("AI service is not configured");
267+
}
268+
228269
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
229270

230271
const prompt = `Explain the concept of "${concept}" in the context of ${language} programming.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import * as Y from 'yjs';
2+
import * as awarenessProtocol from 'y-protocols/awareness';
3+
import { redisRepo } from '../repos/redisRepo.js';
4+
5+
// In-memory store for Yjs documents
6+
const ydocs = new Map();
7+
8+
/**
9+
* Get or create a Yjs document for a session
10+
*/
11+
export function getYDoc(sessionId) {
12+
if (ydocs.has(sessionId)) {
13+
return ydocs.get(sessionId);
14+
}
15+
16+
const ydoc = new Y.Doc();
17+
ydoc.on('update', async (update, origin) => {
18+
if (origin !== 'redis') {
19+
// Save update to Redis when document changes
20+
try {
21+
const key = `collab:yjs:${sessionId}`;
22+
await redisRepo.setJson(key, {
23+
update: Array.from(update),
24+
timestamp: Date.now()
25+
});
26+
} catch (err) {
27+
console.error('[YjsService] Failed to save to Redis:', err);
28+
}
29+
}
30+
});
31+
32+
ydocs.set(sessionId, ydoc);
33+
return ydoc;
34+
}
35+
36+
/**
37+
* Load a Yjs document from Redis
38+
*/
39+
export async function loadYDocFromRedis(sessionId) {
40+
const ydoc = getYDoc(sessionId);
41+
42+
try {
43+
const key = `collab:yjs:${sessionId}`;
44+
const stored = await redisRepo.getJson(key);
45+
46+
if (stored && stored.update) {
47+
// Apply the stored update without triggering the update handler
48+
Y.applyUpdate(ydoc, new Uint8Array(stored.update));
49+
}
50+
} catch (err) {
51+
console.error('[YjsService] Failed to load from Redis:', err);
52+
}
53+
54+
return ydoc;
55+
}
56+
57+
/**
58+
* Handle a Yjs update from a client
59+
*/
60+
export async function applyYjsUpdate(sessionId, update, userId) {
61+
const ydoc = getYDoc(sessionId);
62+
63+
try {
64+
// Apply the update
65+
Y.applyUpdate(ydoc, new Uint8Array(update), 'client');
66+
67+
// Get the text content from the document (using 'monaco' key to match frontend)
68+
const ytext = ydoc.getText('monaco');
69+
const text = ytext.toString();
70+
71+
// Also save to old Redis key for backward compatibility with test execution
72+
try {
73+
await redisRepo.setJson(`collab:document:${sessionId}`, {
74+
text: text,
75+
version: Date.now() // Use timestamp as version
76+
});
77+
console.log(`[YjsService] Saved text to collab:document:${sessionId}`);
78+
} catch (err) {
79+
console.error('[YjsService] Failed to save to old key:', err);
80+
}
81+
82+
return {
83+
success: true,
84+
text,
85+
version: ydoc.transactionOrigin
86+
};
87+
} catch (err) {
88+
console.error('[YjsService] Failed to apply update:', err);
89+
return {
90+
success: false,
91+
error: err.message
92+
};
93+
}
94+
}
95+
96+
/**
97+
* Create an awareness instance for a session
98+
*/
99+
const awarenessInstances = new Map();
100+
101+
export function getAwareness(sessionId) {
102+
if (awarenessInstances.has(sessionId)) {
103+
return awarenessInstances.get(sessionId);
104+
}
105+
106+
const ydoc = getYDoc(sessionId);
107+
const awareness = new awarenessProtocol.Awareness(ydoc);
108+
awarenessInstances.set(sessionId, awareness);
109+
110+
return awareness;
111+
}
112+
113+
/**
114+
* Clean up resources for a session
115+
*/
116+
export function cleanupSession(sessionId) {
117+
ydocs.delete(sessionId);
118+
awarenessInstances.delete(sessionId);
119+
}

0 commit comments

Comments
 (0)