Skip to content

Commit 7286534

Browse files
authored
Merge pull request #74 from SPWwj/main
Collab Service
2 parents ad4be0d + 84ba7cc commit 7286534

File tree

10 files changed

+250
-72
lines changed

10 files changed

+250
-72
lines changed

backend/user-service/services/tokenService.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { UnauthorisedError } from "../utils/httpErrors.js";
77
class TokenService {
88
static generateAccessToken(user) {
99
const accessToken = jwt.sign(
10-
{ id: user.id, isAdmin: user.isAdmin },
10+
{ id: user.id, isAdmin: user.isAdmin, username: user.username },
1111
jwtConfig.accessTokenSecret,
1212
jwtConfig.accessTokenOptions
1313
);
@@ -16,7 +16,7 @@ class TokenService {
1616

1717
static generateRefreshToken(user) {
1818
const refreshToken = jwt.sign(
19-
{ id: user.id, jti: uuidv4() },
19+
{ id: user.id, jti: uuidv4(), username: user.username },
2020
jwtConfig.refreshTokenSecret,
2121
jwtConfig.refreshTokenOptions
2222
);

frontend/src/domain/context/CollaborationContext.tsx

Lines changed: 38 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { createContext, useState, useContext, ReactNode, useEffect, useMemo } from "react";
1+
import React, { createContext, useState, useContext, ReactNode, useEffect, useMemo, useCallback } from "react";
22
import * as Y from "yjs";
33
import * as monaco from "monaco-editor";
44
import { MonacoBinding } from "y-monaco";
@@ -20,6 +20,7 @@ interface CollaborationContextType {
2020
execResult: CodeExecResult | null;
2121
setRoomId: (roomId: string) => void;
2222
connectedUsers: string[];
23+
disconnect: () => void;
2324
}
2425

2526
const CollaborationContext = createContext<CollaborationContextType | undefined>(undefined);
@@ -41,86 +42,81 @@ export const CollaborationProvider: React.FC<{ children: ReactNode }> = ({ child
4142
const ymap: Y.Map<any> = useMemo(() => ydoc.getMap("sharedMap"), [ydoc]);
4243

4344
const [roomId, setRoomId] = useState<string | null>(null);
44-
4545
const [languages, setLanguages] = useState<Language[]>([]);
4646
const [execResult, setExecResult] = useState<CodeExecResult | null>(null);
4747
const [isExecuting, setIsExecuting] = useState<boolean>(false);
4848
const [connectedUsers, setConnectedUsers] = useState<string[]>([]);
49-
5049
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
5150
const [provider, setProvider] = useState<WebsocketProvider | null>(null);
5251
const [binding, setBinding] = useState<MonacoBinding | null>(null);
5352

54-
// This effect manages the lifetime of the yjs doc and the provider
53+
const disconnect = useCallback(() => {
54+
binding?.destroy();
55+
provider?.destroy();
56+
ydoc?.destroy();
57+
setConnectedUsers([]);
58+
setProvider(null);
59+
setBinding(null);
60+
setRoomId(null);
61+
}, [binding, provider, ydoc]);
62+
5563
useEffect(() => {
56-
if (roomId == null) {
57-
return;
58-
}
59-
const provider = new WebsocketProvider("ws://localhost:1234", roomId, ydoc);
60-
setProvider(provider);
61-
62-
provider.awareness.setLocalStateField(USERNAME, username);
63-
provider.awareness.on("change", (update: any) => {
64-
const users = Array.from(provider.awareness.getStates().values());
65-
setConnectedUsers(users.map((user) => user[USERNAME]));
66-
// TODO: Some UI feedback about connection status of the other user
64+
if (!roomId) return;
65+
66+
const newProvider = new WebsocketProvider("ws://localhost:1234", roomId, ydoc);
67+
setProvider(newProvider);
68+
69+
newProvider.awareness.setLocalStateField(USERNAME, username);
70+
newProvider.awareness.on("change", () => {
71+
const users = Array.from(newProvider.awareness.getStates().values());
72+
const uniqueUsers = new Set(users.map((user) => user[USERNAME])); // Use Set for uniqueness
73+
setConnectedUsers(Array.from(uniqueUsers)); // Convert Set back to Array
6774
});
6875

6976
return () => {
70-
provider?.destroy();
71-
ydoc?.destroy();
77+
disconnect();
7278
};
73-
}, [ydoc, roomId]);
79+
}, [ydoc, roomId, username, USERNAME, disconnect]);
7480

75-
// This effect manages the lifetime of the editor binding
7681
useEffect(() => {
77-
if (provider == null || editor == null || editor.getModel() == null) {
78-
return;
79-
}
82+
if (!provider || !editor?.getModel()) return;
8083

81-
const binding = new MonacoBinding(
84+
const newBinding = new MonacoBinding(
8285
ydoc.getText("monaco"),
8386
editor.getModel()!,
8487
new Set([editor]),
85-
provider?.awareness
88+
provider.awareness
8689
);
87-
88-
setBinding(binding);
90+
setBinding(newBinding);
8991

9092
ymap.observe((event) => {
9193
event.changes.keys.forEach((change, key) => {
9294
if (key === SELECTED_LANGUAGE) {
9395
const language: Language = ymap.get(SELECTED_LANGUAGE);
9496
setSelectedLanguage(language);
95-
const model = editor.getModel();
96-
monaco.editor.setModelLanguage(model!, language.language);
97+
monaco.editor.setModelLanguage(editor.getModel()!, language.language);
9798
}
9899
});
99100
});
100101

101-
// Set the editor's language
102102
const language: Language = ymap.get(SELECTED_LANGUAGE);
103-
const model = editor.getModel();
104-
monaco.editor.setModelLanguage(model!, language?.language ?? "javascript");
103+
monaco.editor.setModelLanguage(editor.getModel()!, language?.language ?? "javascript");
105104

106-
return () => {
107-
binding.destroy();
108-
};
109-
}, [ydoc, provider, editor, ymap]);
105+
return () => newBinding.destroy();
106+
}, [ydoc, provider, editor, ymap, SELECTED_LANGUAGE]);
110107

111108
useEffect(() => {
112109
initialiseLanguages();
113110
}, []);
114111

115112
const initialiseLanguages = async () => {
116-
// Initialise language dropdown
117113
const allLanguages = monaco.languages.getLanguages();
118114
const pistonLanguageVersions = await PistonClient.getLanguageVersions();
119115
setLanguages(
120116
allLanguages
121117
.filter((lang) => pistonLanguageVersions.some((pistonLang: any) => pistonLang.language === lang.id))
122118
.map((lang) => ({
123-
alias: lang.aliases && lang.aliases.length > 0 ? lang.aliases[0] : lang.id,
119+
alias: lang.aliases?.[0] || lang.id,
124120
language: lang.id,
125121
version: pistonLanguageVersions.find((pistonLang: any) => pistonLang.language === lang.id)?.version
126122
}))
@@ -135,11 +131,9 @@ export const CollaborationProvider: React.FC<{ children: ReactNode }> = ({ child
135131
try {
136132
setIsExecuting(true);
137133
const sourceCode = editor?.getValue();
138-
if (!sourceCode) {
139-
// TODO
140-
return;
141-
}
142-
const output: CodeExecResult = await PistonClient.executeCode(selectedLanguage, sourceCode);
134+
if (!sourceCode) return;
135+
136+
const output = await PistonClient.executeCode(selectedLanguage, sourceCode);
143137
setExecResult(output);
144138
} catch (e) {
145139
toast.error("There was an issue running the code");
@@ -163,7 +157,8 @@ export const CollaborationProvider: React.FC<{ children: ReactNode }> = ({ child
163157
handleExecuteCode,
164158
isExecuting,
165159
execResult,
166-
connectedUsers
160+
connectedUsers,
161+
disconnect
167162
}}
168163
>
169164
{children}
Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
.submitButton {
2-
background-color: #ffa500;
2+
background-color: #ffa500 !important;
3+
color: black !important;
4+
font-weight: bold !important;
35
}
46

57
.runButton {
6-
background-color: black;
7-
color: white;
8+
background-color: #1a1a1a !important;
9+
color: white !important;
10+
}
11+
12+
.runButton:hover {
13+
background-color: #333 !important;
14+
}
15+
16+
.submitButton:hover {
17+
background-color: #ffb52e !important;
818
}
919

1020
button {
1121
width: fit-content;
12-
}
22+
}

frontend/src/presentation/components/CodeEditor/CodeEditor.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useState } from "react";
1+
import React, { useState, useEffect, useRef } from "react";
22
import styles from "./CodeEditor.module.css";
3-
import Editor, { Monaco } from "@monaco-editor/react";
3+
import Editor from "@monaco-editor/react";
44
import { Button, Spin } from "antd";
55
import { useCollaboration } from "domain/context/CollaborationContext";
66
import * as monaco from "monaco-editor";
@@ -15,22 +15,47 @@ interface CodeEditorProps {
1515
const CodeEditor: React.FC<CodeEditorProps> = ({ roomId }) => {
1616
const { onEditorIsMounted, isExecuting, setRoomId, connectedUsers } = useCollaboration();
1717
const [theme, setTheme] = useState("vs-light");
18+
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
19+
const containerRef = useRef<HTMLDivElement>(null);
20+
21+
useEffect(() => {
22+
let resizeObserver: ResizeObserver | null = null;
23+
let resizeTimeout: NodeJS.Timeout;
24+
25+
if (containerRef.current && editorRef.current) {
26+
resizeObserver = new ResizeObserver((entries) => {
27+
// Debounce resize events
28+
clearTimeout(resizeTimeout);
29+
resizeTimeout = setTimeout(() => {
30+
if (editorRef.current) {
31+
editorRef.current.layout();
32+
}
33+
}, 100);
34+
});
35+
36+
resizeObserver.observe(containerRef.current);
37+
}
38+
39+
return () => {
40+
if (resizeObserver) {
41+
resizeObserver.disconnect();
42+
}
43+
clearTimeout(resizeTimeout);
44+
};
45+
}, []);
1846

1947
const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
48+
editorRef.current = editor;
2049
onEditorIsMounted(editor);
2150
setRoomId(roomId);
2251
};
2352

2453
const handleToggleTheme = () => {
25-
if (theme === "vs-light") {
26-
setTheme("vs-dark");
27-
} else {
28-
setTheme("vs-light");
29-
}
54+
setTheme(theme === "vs-light" ? "vs-dark" : "vs-light");
3055
};
3156

3257
return (
33-
<div className={styles.container}>
58+
<div className={styles.container} ref={containerRef}>
3459
<div className={styles.toolbar}>
3560
<div className={styles.toolbarLeft}>
3661
<LanguageSelector />
@@ -69,7 +94,8 @@ const CodeEditor: React.FC<CodeEditorProps> = ({ roomId }) => {
6994
options={{
7095
minimap: { enabled: false },
7196
scrollbar: { verticalScrollbarSize: 4 },
72-
formatOnPaste: true
97+
formatOnPaste: true,
98+
automaticLayout: false // Disable automatic layout
7399
}}
74100
/>
75101
</div>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.toggleContainer {
2+
display: flex;
3+
align-items: center;
4+
justify-content: space-between;
5+
padding: 12px 16px;
6+
}
7+
8+
.toggleButton {
9+
display: inline-flex;
10+
align-items: center;
11+
gap: 8px;
12+
padding: 8px 16px;
13+
border-radius: 8px;
14+
font-size: 14px;
15+
font-weight: 500;
16+
color: #333;
17+
background: transparent;
18+
border: none;
19+
cursor: pointer;
20+
transition: all 0.2s ease;
21+
}
22+
23+
.toggleButton:hover {
24+
background-color: #f5f5f5;
25+
}
26+
27+
.icon {
28+
width: 16px;
29+
height: 16px;
30+
}
31+
32+
.status {
33+
padding: 4px 8px;
34+
border-radius: 12px;
35+
font-size: 12px;
36+
font-weight: 500;
37+
}
38+
39+
.chatMode {
40+
background-color: #e8f0fe;
41+
color: #1a73e8;
42+
}
43+
44+
.questionMode {
45+
background-color: #f3e8ff;
46+
color: #7e22ce;
47+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// ToggleButton.tsx
2+
import React from "react";
3+
import styles from "./ToggleButton.module.css";
4+
5+
const ToggleButton = ({ showChat, onClick }: { showChat: boolean; onClick: () => void }) => {
6+
return (
7+
<div className={styles.toggleContainer}>
8+
<button className={styles.toggleButton} onClick={onClick}>
9+
<span className={styles.iconWrapper}>
10+
{showChat ? (
11+
<span className={`${styles.icon} ${styles.bookIcon}`}>📖</span>
12+
) : (
13+
<span className={`${styles.icon} ${styles.chatIcon}`}>💬</span>
14+
)}
15+
</span>
16+
<span>{showChat ? "View Question" : "Open Chat"}</span>
17+
</button>
18+
<div className={`${styles.status} ${showChat ? styles.chatMode : styles.questionMode}`}>
19+
{showChat ? "Chat Mode" : "Question Mode"}
20+
</div>
21+
</div>
22+
);
23+
};
24+
25+
export default ToggleButton;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.chatFrameWrapper {
2+
height: 100%;
3+
width: 100%;
4+
display: flex;
5+
flex-direction: column;
6+
background: #ffffff;
7+
border-radius: 8px;
8+
overflow: hidden;
9+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
10+
}
11+
12+
.chatFrame {
13+
width: 100%;
14+
flex: 1;
15+
border: none;
16+
background: transparent;
17+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// ChatFrame.tsx
2+
import React from "react";
3+
import styles from "./ChatFrame.module.css";
4+
5+
interface ChatFrameProps {
6+
roomId: string;
7+
}
8+
9+
const ChatFrame: React.FC<ChatFrameProps> = ({ roomId }) => {
10+
const chatUrl = `http://localhost:5173/Assistant`;
11+
12+
return (
13+
<div className={styles.chatFrameWrapper}>
14+
<iframe
15+
src={chatUrl}
16+
className={styles.chatFrame}
17+
title="Chat Embed"
18+
allow="camera; microphone; fullscreen; display-capture"
19+
/>
20+
</div>
21+
);
22+
};
23+
24+
export default ChatFrame;

0 commit comments

Comments
 (0)