Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5dcf80c
Merge pull request #1 from CS3219-AY2526Sem1/dev
k1b1t0 Oct 9, 2025
d07e614
Add POST API for execution service
Oct 11, 2025
fef5cbe
Rewrite execution service to running code only
Oct 13, 2025
84eb792
WIP: latest changes before merging dev
Oct 16, 2025
0e9ec2b
Merge branch 'dev' into execution-service
k1b1t0 Oct 16, 2025
33d6fbb
Modify docker-compose.yml
k1b1t0 Oct 21, 2025
ecd63ef
Merge branch 'dev' into execution-service
k1b1t0 Oct 21, 2025
33b3128
Merge collaboration service and change execution-service
k1b1t0 Oct 23, 2025
5d89414
feat(collaboration-service-frontend):
ncduy0303 Oct 23, 2025
0374254
fix(code-editor-panel): dynamic loading monaco editor on client
ncduy0303 Oct 23, 2025
de9bb4e
Error handling when worker can't connect to PistonAPI
k1b1t0 Oct 24, 2025
6e2086e
Resolve confict with dev branch
k1b1t0 Oct 24, 2025
78f2dcb
Merge branch 'execution-service' into video-call-service
k1b1t0 Oct 28, 2025
0e3b49b
Add Agora Token generator
k1b1t0 Oct 28, 2025
9e445d0
Add CI workflow for execution service
k1b1t0 Oct 28, 2025
0a4cebb
Merge branch 'collaboration-service-frontend' into integrate-executio…
k1b1t0 Oct 29, 2025
01b072d
Integrate collaboration and execution, todo: broadcast to FE, and sen…
k1b1t0 Oct 29, 2025
fcb2507
Integrate code execution to collab FE
k1b1t0 Nov 5, 2025
1ab02bf
Merge remote-tracking branch 'origin' into integrate-execution-collab…
k1b1t0 Nov 5, 2025
f6befac
Fix rabbitMQ startup error
k1b1t0 Nov 5, 2025
3bf1768
Add sync error output when execution dies
k1b1t0 Nov 6, 2025
484373e
Merge video-call branch
k1b1t0 Nov 6, 2025
89c148f
Merge branch 'dev' into integrate-execution-collaboration
k1b1t0 Nov 6, 2025
e283487
Fix lock file
k1b1t0 Nov 6, 2025
77b0f6b
Clean execution service
k1b1t0 Nov 6, 2025
851da93
Change buildToken function
k1b1t0 Nov 7, 2025
22ae4cb
Clean execution service and add .env example
k1b1t0 Nov 7, 2025
aa9e19f
Add .env example
k1b1t0 Nov 7, 2025
fa7ed6f
Update README to include installation instructions for Piston languag…
DanielJames0302 Nov 7, 2025
97f9122
Fix camera panel
k1b1t0 Nov 8, 2025
1334ad9
Fix error message
k1b1t0 Nov 8, 2025
60195cb
Merge branch 'integrate-execution-collaboration' into video-call-service
k1b1t0 Nov 8, 2025
b84ca6f
Add error handling when video-call-service dies
k1b1t0 Nov 9, 2025
27e0eb5
Merge branch 'dev' into video-call-service
k1b1t0 Nov 9, 2025
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
3 changes: 3 additions & 0 deletions backend/video-call-service/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
APP_ID=997489f851fa4186a4b9c65bab955777
APP_CERTIFICATE=63c44822bae140a3ade7153d209233f5
PORT=8011
31 changes: 31 additions & 0 deletions backend/video-call-service/Dockerfile.video
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Use Node.js 20 Alpine as base image
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Install pnpm globally
RUN npm install -g [email protected]

# Copy package files
COPY package.json ./

# Install dependencies
RUN pnpm install --no-frozen-lockfile

# Copy source code
COPY . .

# Remove dev dependencies to reduce image size
RUN pnpm prune --prod

# Create non-root user for security
RUN addgroup -g 1001 -S nodejs
RUN adduser -S video-call-service -u 1011

# Change ownership of the app directory
RUN chown -R video-call-service:nodejs /app
USER video-call-service

# Start the application
CMD ["pnpm", "start"]
27 changes: 14 additions & 13 deletions backend/video-call-service/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
const express = require('express');
const {RtcTokenBuilder, RtcRole} = require('agora-access-token');
const dotenv = require('dotenv');

dotenv.config()
const app = express()

const PORT = process.env.PORT
Expand All @@ -23,32 +20,36 @@ const generateRTCToken = (req, resp) => {
if (!roomId) {
return resp.status(500).json({ 'error': 'roomId is required' });
}

// user id
let uid = req.params.uid;
if(!uid || uid === '') {
return resp.status(500).json({ 'error': 'uid is required' });
}

// get role
let role = RtcRole.PUBLISHER
console.log(">>> Role is always PUBLISHER")
// check expire time for token
let expireTime = req.query.expiry;
if (!expireTime || expireTime === '') {
expireTime = 3600;
} else {
expireTime = parseInt(expireTime, 10);
}

// set expire time for token
let expireTime = 3600

// calculate expire time
const currentTime = Math.floor(Date.now() / 1000);
const privilegeExpireTime = currentTime + expireTime;

// build token
let token = RtcTokenBuilder.buildTokenWithUid(APP_ID, APP_CERTIFICATE, roomId, uid, role, privilegeExpireTime)
console.log(">>> Create token successfully!")
let token = RtcTokenBuilder.buildTokenWithAccount(APP_ID, APP_CERTIFICATE, roomId, uid, role, privilegeExpireTime)
console.log(">>> Build Token with Account")
console.log(roomId)
console.log(uid)
console.log(role)

// return token
return resp.json({ 'rtcToken': token });
}

app.get('/v1/rtc/:roomid/:uid', nocache , generateRTCToken)
app.get('/v1/video/:roomid/:uid', nocache , generateRTCToken)

app.listen(PORT, () => {
console.log(`Listening on port: ${PORT}`);
Expand Down
16 changes: 15 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ services:
build:
context: ./backend/execution-service
dockerfile: Dockerfile.execution
container_name: execution-serivce
container_name: execution-service
restart: always
env_file:
- ./backend/execution-service/.env
Expand All @@ -160,6 +160,20 @@ services:
networks:
- cs3219-peerprep-network
command: pnpm run start

video-call-service:
build:
context: ./backend/video-call-service
dockerfile: Dockerfile.video
container_name: video-call-service
restart: always
env_file:
- ./backend/video-call-service/.env
ports:
- '8011:8011'
networks:
- cs3219-peerprep-network
command: pnpm run start
piston-api:
image: ghcr.io/engineer-man/piston:latest
container_name: piston-api
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-query": "^5.90.2",
"agora-rtc-react": "^2.5.0",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
205 changes: 147 additions & 58 deletions frontend/src/components/practice/communication-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,166 @@
"use client";

import { useEffect, useState } from "react";
import { Separator } from "@/components/ui/separator";
import { Card, CardTitle, CardContent, CardHeader } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Mic, Video } from "lucide-react";
import { Mic, MicOff, Video, VideoOff } from "lucide-react";
import AgoraRTC, {
AgoraRTCProvider,
LocalVideoTrack,
RemoteUser,
useJoin,
useLocalCameraTrack,
useLocalMicrophoneTrack,
usePublish,
useRTCClient,

Check warning on line 16 in frontend/src/components/practice/communication-panel.tsx

View workflow job for this annotation

GitHub Actions / Frontend Lint, Typecheck & Build

'useRTCClient' is defined but never used
useRemoteAudioTracks,

Check warning on line 17 in frontend/src/components/practice/communication-panel.tsx

View workflow job for this annotation

GitHub Actions / Frontend Lint, Typecheck & Build

'useRemoteAudioTracks' is defined but never used
useRemoteUsers,
} from "agora-rtc-react";
import { useCollaborationActions, useCollaborationStore } from "@/stores/collaboration-store";
import { useAuthContext } from "@/contexts/auth-context";

const client = AgoraRTC.createClient({ codec: "vp8", mode: "rtc" });
const AGORA_APP_ID = "997489f851fa4186a4b9c65bab955777";

function AgoraVideoCall(props: { userId: string | undefined }) {
const { userId } = props;

const { roomDetails, agoraToken } = useCollaborationStore();

const [micOn, setMicOn] = useState(true);
const [camOn, setCamOn] = useState(true);

// mic, cam
const { localMicrophoneTrack } = useLocalMicrophoneTrack();
const { localCameraTrack } = useLocalCameraTrack();

const remoteUsers = useRemoteUsers();
console.log(">>> Agora connection: ", remoteUsers.length);

usePublish([localMicrophoneTrack, localCameraTrack]);

useJoin(
{
appid: AGORA_APP_ID,
channel: roomDetails?.roomId || "",
token: agoraToken,
uid: userId,
},
!!agoraToken && !!userId,
);

useEffect(() => {
if (localMicrophoneTrack) {
localMicrophoneTrack.setEnabled(micOn);
}
}, [micOn, localMicrophoneTrack]);

useEffect(() => {
if (localCameraTrack) {
localCameraTrack.setEnabled(camOn);
}
}, [camOn, localCameraTrack]);

return (
<>
<div className="flex flex-row gap-2 flex-1 w-full">
<div className="flex-1 bg-gray-900 rounded-md h-48 flex items-center justify-center relative overflow-hidden">
<div className="absolute top-4 right-4 z-10 flex gap-2">
<Button
variant={micOn ? "default" : "destructive"}
className="w-[50px] h-[35px]"
onClick={() => setMicOn((prev) => !prev)}
>
{micOn ? <Mic /> : <MicOff />}
</Button>
<Button
variant={camOn ? "default" : "destructive"}
className="w-[50px] h-[35px]"
onClick={() => setCamOn((prev) => !prev)}
>
{camOn ? <Video /> : <VideoOff />}
</Button>
</div>
<span className="text-gray-400 absolute top-2 left-2 z-10 text-xs">
{userId || "You"}
</span>
{camOn ? (
<LocalVideoTrack
track={localCameraTrack}
play={true}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
) : (
<span className="text-gray-500">Camera Off</span>
)}
</div>

<div className="flex-1 bg-gray-900 rounded-md h-48 flex items-center justify-center relative overflow-hidden">
{remoteUsers.length > 0 ? (
remoteUsers.map((user) => (
<div key={user.uid} className="w-full h-full">
<span className="text-gray-400 absolute top-2 left-2 z-10 text-xs">
{user.uid.toString() || "Partner"}
</span>
{user.hasVideo ? (
<RemoteUser
user={user}
playAudio={true}
playVideo={true}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
) : (
<>
<RemoteUser user={user} playAudio={true} playVideo={false} />
<div className="w-full h-full flex items-center justify-center">
<span className="text-gray-500">Camera Off</span>
</div>
</>
)}
</div>
))
) : (
<span className="text-gray-500">Waiting...</span>
)}
</div>
</div>
</>
);
}

export default function CommunicationPanel() {
const { roomDetails, agoraToken } = useCollaborationStore();
const { fetchAgoraToken } = useCollaborationActions();
const { user } = useAuthContext();
const userId = user?.username;

useEffect(() => {
if (roomDetails?.roomId && userId && !agoraToken) {
fetchAgoraToken(roomDetails.roomId, userId);
}
}, [roomDetails?.roomId, userId, agoraToken, fetchAgoraToken]);

return (
<Card className="rounded-none min-h-full h-auto w-full flex flex-col">
{/* Header */}
<CardHeader className="flex items-center justify-between">
<CardTitle>Communication</CardTitle>
<div className="flex gap-2">
<Button variant="default" className="w-[100px] h-[35px]">
<Mic />
</Button>
<Button variant="default" className="w-[100px] h-[35px]">
<Video />
</Button>
</div>
</CardHeader>

<Separator />

{/* Video + Chat Section */}
<CardContent className="flex flex-col md:flex-row gap-4 h-full">
<CardContent className="flex flex-row gap-4 h-full relative p-2">
{/* Video Feeds stacked vertically */}
<div className="flex flex-col gap-2 flex-1">
<div className="flex-1 bg-gray-200 rounded-md h-48 flex items-center justify-center">
<span className="text-gray-500">User 1</span>
{AGORA_APP_ID && roomDetails && userId && agoraToken ? (
<AgoraRTCProvider client={client}>
<AgoraVideoCall userId={userId} />
</AgoraRTCProvider>
) : (
<div className="flex items-center justify-center w-full h-48">
<span className="text-gray-500">Loading video</span>
</div>
<div className="flex-1 bg-gray-200 rounded-md h-48 flex items-center justify-center">
<span className="text-gray-500">User 2</span>
</div>
</div>

{/* Chat Section */}
<div className="flex-1 bg-gray-50 rounded-md p-2 h-96 flex flex-col">
{/* Chat Messages Placeholder */}
<div className="flex-1 overflow-y-auto mb-2 flex flex-col gap-2">
{/* User 1 (left) */}
<div className="flex">
<div className="bg-gray-200 p-2 rounded-md max-w-[70%]">{"Hello! How are you?"}</div>
</div>

{/* User 2 (right) */}
<div className="flex justify-end">
<div className="bg-accent p-2 rounded-md max-w-[70%]">{"I'm good, thanks!"}</div>
</div>

{/* User 1 */}
<div className="flex">
<div className="bg-gray-200 p-2 rounded-md max-w-[70%]">
{"Ready for the meeting?"}
</div>
</div>

{/* User 2 */}
<div className="flex justify-end">
<div className="bg-accent p-2 rounded-md max-w-[70%]">{"Yes, let's start."}</div>
</div>
</div>

{/* Input Box */}
<div className="flex gap-2">
<input
type="text"
placeholder="Type a message..."
className="flex-1 border rounded-md p-1"
/>
<Button variant="default" className="w-[80px]">
Send
</Button>
</div>
</div>
)}
</CardContent>
</Card>
);
Expand Down
Loading