Skip to content
Merged
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
38 changes: 38 additions & 0 deletions backend/controllers/audioPipelineController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// audioPipelineController.js
// Handles full-duplex WebSocket connections for STT/LLM/TTS streaming.

const setupAudioPipeline = (wss) => {
wss.on('connection', (ws) => {
console.log('New Audio Pipeline WebSocket Connection Established');

// In a real implementation, we would pipe these chunks to a streaming STT API
// (e.g., Whisper, Deepgram, Google Speech-to-Text).
ws.on('message', (message) => {
// 1. Receive binary audio chunks from the frontend client.
// 2. STT Provider converts to text.

// MOCK: Echo back a mock transcript and simulated audio chunk
const mockTranscript = "That's a great question regarding system design.";

// Send the text transcript back to the frontend for UI display
ws.send(JSON.stringify({ type: 'transcript', text: mockTranscript }));

// 3. The transcript is passed to the LLM (e.g., OpenAI, Gemini) to generate an AI response.
// 4. The LLM response is passed to a TTS Provider (e.g., ElevenLabs) to generate audio.

// MOCK: Send simulated binary audio bytes back to the client
const mockAudioBytes = Buffer.from('mock_audio_bytes_representing_speech');
ws.send(mockAudioBytes);
});

ws.on('close', () => {
console.log('Audio Pipeline WebSocket Connection Closed');
});

ws.on('error', (error) => {
console.error('Audio Pipeline WebSocket Error:', error);
});
});
};

module.exports = { setupAudioPipeline };
33 changes: 33 additions & 0 deletions backend/controllers/codeExecutionController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const DockerSandboxService = require("../services/dockerSandboxService");

/**
* Controller to handle code execution requests from the frontend.
*/
const executeCode = async (req, res) => {
const { code, language } = req.body;

if (!code || !language) {
return res.status(400).json({
success: false,
message: "Both 'code' and 'language' fields are required.",
});
}

try {
const result = await DockerSandboxService.executeCode(code, language.toLowerCase());

res.status(200).json({
success: true,
data: result,
});
} catch (error) {
console.error("Code Execution Error:", error);
res.status(500).json({
success: false,
message: "An error occurred while executing the code in the sandbox.",
error: error.message,
});
}
};

module.exports = { executeCode };
35 changes: 35 additions & 0 deletions backend/models/RepetitionState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const mongoose = require("mongoose");

const repetitionStateSchema = new mongoose.Schema(
{
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
flashcardId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Flashcard",
required: true,
},
easinessFactor: {
type: Number,
default: 2.5,
},
interval: {
type: Number,
default: 0,
},
repetitions: {
type: Number,
default: 0,
},
nextReviewDate: {
type: Date,
default: Date.now,
},
},
{ timestamps: true }
);

module.exports = mongoose.model("RepetitionState", repetitionStateSchema);
10 changes: 10 additions & 0 deletions backend/routes/codeExecutionRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const express = require("express");
const router = express.Router();
const { executeCode } = require("../controllers/codeExecutionController");
const { protect } = require("../middlewares/authMiddleware");

// Route: POST /api/execute
// Protected route to execute code in the Docker sandbox
router.post("/", protect, executeCode);

module.exports = router;
38 changes: 38 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const googleCalendarRoutes = require("./routes/googleCalendarRoutes");
const path = require("path");
const connectDB = require("./config/db");
const cookieParser = require("cookie-parser");
const WebSocket = require("ws");
const { setupAudioPipeline } = require("./controllers/audioPipelineController");
const {
generateInterviewQuestions,
generateConceptExplanation,
Expand Down Expand Up @@ -160,6 +162,8 @@ const roadmapRoutes = require("./routes/roadmapRoutes");
app.use("/api/roadmaps", roadmapRoutes);
const interviewExperienceRoutes = require("./routes/interviewExperienceRoutes");
app.use("/api/interview-experiences", generalLimiter, interviewExperienceRoutes);
const codeExecutionRoutes = require("./routes/codeExecutionRoutes");
app.use("/api/execute", generalLimiter, codeExecutionRoutes);
const bookmarkRoutes = require("./routes/bookmarkRoutes");
app.use("/api/bookmarks", generalLimiter, bookmarkRoutes);

Expand All @@ -183,6 +187,25 @@ if (process.env.ADZUNA_APP_ID && process.env.ADZUNA_API_KEY) {
setInterval(refreshJobCache, 24 * 60 * 60 * 1000);
}

// Spaced Repetition CRON logic
const RepetitionState = require("./models/RepetitionState");
const runSpacedRepetitionCron = async () => {
try {
const today = new Date();
// This will aggregate and flag "due" cards for users.
// In a full implementation, you would update user queues here.
const dueCards = await RepetitionState.find({ nextReviewDate: { $lte: today } });
if (dueCards.length > 0) {
console.log(`[CRON] Found ${dueCards.length} due flashcards for spaced repetition review.`);
}
} catch (error) {
console.error("[CRON] Spaced repetition error:", error);
}
};
// Run the spaced repetition job every 24 hours
setInterval(runSpacedRepetitionCron, 24 * 60 * 60 * 1000);


// Start Server
const PORT = process.env.PORT || 5000;
const server = app.listen(PORT, "0.0.0.0", () => {
Expand All @@ -197,6 +220,21 @@ const server = app.listen(PORT, "0.0.0.0", () => {
}
});

// Attach WebSocket Server
const wss = new WebSocket.Server({ noServer: true });
server.on("upgrade", (request, socket, head) => {
if (request.url === "/api/audio-stream") {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
} else {
socket.destroy();
}
});

// Initialize the audio pipeline handler
setupAudioPipeline(wss);

server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
console.error(
Expand Down
77 changes: 77 additions & 0 deletions backend/services/dockerSandboxService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const { exec } = require("child_process");
const fs = require("fs").promises;
const path = require("path");
const crypto = require("crypto");

/**
* Service to execute raw user code in an isolated Docker container.
*
* In a real production environment, this leverages Dockerode or
* direct Docker CLI commands with strict --memory and --cpus limits.
*/
class DockerSandboxService {
/**
* Executes code in a sandbox.
*
* @param {string} code - The raw source code to execute.
* @param {string} language - The programming language (e.g., 'python', 'javascript').
* @returns {Promise<{stdout: string, stderr: string}>} - The execution results.
*/
static async executeCode(code, language) {
const runId = crypto.randomBytes(8).toString("hex");
let fileName, dockerImage, runCommand;

// Map language to docker container config
if (language === "python") {
fileName = `script_${runId}.py`;
dockerImage = "python:3.9-alpine";
runCommand = `python /tmp/${fileName}`;
} else if (language === "javascript" || language === "js") {
fileName = `script_${runId}.js`;
dockerImage = "node:18-alpine";
runCommand = `node /tmp/${fileName}`;
} else {
throw new Error(`Unsupported language: ${language}`);
}

const tempFilePath = path.join(__dirname, "..", "tmp", fileName);

try {
// 1. Ensure tmp directory exists
await fs.mkdir(path.join(__dirname, "..", "tmp"), { recursive: true });

// 2. Write the code to a temporary file
await fs.writeFile(tempFilePath, code);

// 3. Execute the code inside an ephemeral Docker container.
// We mount the temp script to /tmp/ in the container with read-only access.
// --rm removes the container after execution.
// --network none disables internet access for security.
// --memory 128m limits RAM.

const dockerCmd = `docker run --rm --network none --memory 128m -v "${tempFilePath}:/tmp/${fileName}:ro" ${dockerImage} sh -c "${runCommand}"`;

return new Promise((resolve, reject) => {
// Strict 5-second timeout on the child process to prevent infinite loops
exec(dockerCmd, { timeout: 5000 }, async (error, stdout, stderr) => {
// 4. Cleanup temp file
await fs.unlink(tempFilePath).catch(console.error);

if (error && error.killed) {
resolve({ stdout, stderr: "Execution Timeout: Code exceeded the 5-second limit." });
} else if (error) {
resolve({ stdout, stderr: stderr || error.message });
} else {
resolve({ stdout, stderr });
}
});
});
} catch (err) {
// Ensure cleanup on write failure
await fs.unlink(tempFilePath).catch(() => {});
throw err;
}
}
}

module.exports = DockerSandboxService;
47 changes: 47 additions & 0 deletions backend/utils/sm2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SuperMemo-2 Spaced Repetition Algorithm

/**
* Calculates the next state for a flashcard based on SM-2.
*
* @param {number} quality - User's grade (0-5).
* @param {number} repetitions - Current repetitions.
* @param {number} easinessFactor - Current easiness factor.
* @param {number} interval - Current interval (in days).
* @returns {Object} New state containing updated repetitions, easinessFactor, interval, and nextReviewDate.
*/
function calculateSM2(quality, repetitions, easinessFactor, interval) {
let newRepetitions = repetitions;
let newEasinessFactor = easinessFactor;
let newInterval = interval;

if (quality >= 3) {
if (repetitions === 0) {
newInterval = 1;
} else if (repetitions === 1) {
newInterval = 6;
} else {
newInterval = Math.round(interval * easinessFactor);
}
newRepetitions += 1;
} else {
newRepetitions = 0;
newInterval = 1;
}

// Update easiness factor
newEasinessFactor = easinessFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
if (newEasinessFactor < 1.3) newEasinessFactor = 1.3;

// Calculate next review date
const nextReviewDate = new Date();
nextReviewDate.setDate(nextReviewDate.getDate() + newInterval);

return {
repetitions: newRepetitions,
easinessFactor: newEasinessFactor,
interval: newInterval,
nextReviewDate,
};
}

module.exports = { calculateSM2 };
80 changes: 80 additions & 0 deletions frontend/src/hooks/useAudioPipeline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useState, useEffect, useRef } from 'react';

export const useAudioPipeline = (wsUrl) => {
const [isRecording, setIsRecording] = useState(false);
const [transcript, setTranscript] = useState('');
const wsRef = useRef(null);
const mediaRecorderRef = useRef(null);
const audioContextRef = useRef(null);

useEffect(() => {
// Initialize WebSocket connection to the backend
wsRef.current = new WebSocket(wsUrl);

wsRef.current.onopen = () => {
console.log('Connected to Audio Pipeline WebSockets');
};

wsRef.current.onmessage = async (event) => {
// Differentiate between JSON (transcript text) and Blob (audio bytes)
if (typeof event.data === 'string') {
try {
const data = JSON.parse(event.data);
if (data.type === 'transcript') {
setTranscript(data.text);
}
} catch (e) {
console.error("Failed to parse JSON transcript", e);
}
} else if (event.data instanceof Blob) {
// We received binary audio bytes from the TTS API via backend
console.log('Received TTS audio chunk');
await playAudioChunk(event.data);
}
};

return () => {
if (wsRef.current) wsRef.current.close();
};
}, [wsUrl]);

const playAudioChunk = async (blob) => {
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
}
const arrayBuffer = await blob.arrayBuffer();
// In a full implementation, you decode AudioData.
// For now, this is a stub.
console.log(`Stub: Playing audio chunk of size ${arrayBuffer.byteLength} bytes`);
};

const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorderRef.current = new MediaRecorder(stream, { mimeType: 'audio/webm' });

mediaRecorderRef.current.ondataavailable = (event) => {
if (event.data.size > 0 && wsRef.current?.readyState === WebSocket.OPEN) {
// Stream raw audio chunks to backend
wsRef.current.send(event.data);
}
};

// Request chunks every 250ms for low latency
mediaRecorderRef.current.start(250);
setIsRecording(true);
} catch (err) {
console.error('Microphone permission denied', err);
}
};

const stopRecording = () => {
if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
mediaRecorderRef.current.stop();
mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
}
setIsRecording(false);
};

return { isRecording, startRecording, stopRecording, transcript };
};
Loading