Skip to content

Commit a3aef71

Browse files
committed
PEER-219,228,229: Add termination prompt
Signed-off-by: SeeuSim <[email protected]>
1 parent 5e4777a commit a3aef71

File tree

11 files changed

+199
-52
lines changed

11 files changed

+199
-52
lines changed

backend/question/src/routes/question.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const router = Router();
1919

2020
router.get('/search', searchQuestionsByTitle);
2121

22-
router.get('/topic', getTopics);
22+
router.get('/topics', getTopics);
2323
router.get('/difficulties', getDifficulties);
2424

2525
router.get('/', getQuestions);

backend/question/src/services/get/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,13 @@ export const getDifficultiesService = async (): Promise<IGetDifficultiesResponse
228228
};
229229
}
230230

231-
const uniqueDifficulties = results.map((result) => result.difficulty);
232-
231+
const uniqueDifficulties = results
232+
.map((result) => result.difficulty)
233+
.sort((a, b) => {
234+
if (a === 'Hard' || b === 'Easy') return 1;
235+
if (b === 'Hard' || a === 'Easy') return -1;
236+
return 0;
237+
});
233238
return {
234239
code: StatusCodes.OK,
235240
data: {

frontend/src/components/blocks/interview/editor.tsx

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { ChevronLeftIcon } from '@radix-ui/react-icons';
21
import { useWindowSize } from '@uidotdev/usehooks';
32
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
43
import CodeMirror from '@uiw/react-codemirror';
54
import { Bot, User } from 'lucide-react';
6-
import { useMemo, useState } from 'react';
5+
import { useEffect, useMemo, useState } from 'react';
76

87
import { Button } from '@/components/ui/button';
98
import { Label } from '@/components/ui/label';
@@ -18,25 +17,32 @@ import { Skeleton } from '@/components/ui/skeleton';
1817
import { getTheme, type IEditorTheme, languages, themeOptions } from '@/lib/editor/extensions';
1918
import { useCollab } from '@/lib/hooks/use-collab';
2019

20+
import { CompleteDialog } from './room/complete-dialog';
21+
import { OtherUserCompletingDialog } from './room/other-user-completing-dialog';
22+
2123
const EXTENSION_HEIGHT = 250;
2224
const MIN_EDITOR_HEIGHT = 350;
2325

2426
type EditorProps = {
27+
questionId: number;
2528
room: string;
2629
onAIClick: () => void;
2730
onPartnerClick: () => void;
2831
};
2932

30-
export const Editor = ({ room, onAIClick, onPartnerClick }: EditorProps) => {
33+
export const Editor = ({ questionId, room, onAIClick, onPartnerClick }: EditorProps) => {
3134
const { height } = useWindowSize();
3235
const [theme, setTheme] = useState<IEditorTheme>('vscodeDark');
3336
const {
37+
userId,
3438
editorRef,
3539
extensions,
3640
language,
3741
setLanguage,
3842
code,
3943
setCode,
44+
isCompleting,
45+
setIsCompleting,
4046
cursorPosition,
4147
members,
4248
isLoading,
@@ -45,8 +51,19 @@ export const Editor = ({ room, onAIClick, onPartnerClick }: EditorProps) => {
4551
return getTheme(theme);
4652
}, [theme]);
4753

54+
const [isOtherUserCompletingDialogOpen, setIsOtherUserCompletingDialogOpen] = useState('');
55+
56+
useEffect(() => {
57+
if (isCompleting.userId !== userId && isCompleting.state) {
58+
setIsOtherUserCompletingDialogOpen(isCompleting.state);
59+
} else {
60+
setIsOtherUserCompletingDialogOpen('');
61+
}
62+
}, [isCompleting]);
63+
4864
return (
4965
<div className='flex w-full flex-col gap-4 p-4'>
66+
{isOtherUserCompletingDialogOpen && <OtherUserCompletingDialog />}
5067
{isLoading ? (
5168
<div className='flex h-[60px] w-full flex-row justify-between pt-3'>
5269
<div className='flex h-10 flex-row gap-4'>
@@ -93,42 +110,47 @@ export const Editor = ({ room, onAIClick, onPartnerClick }: EditorProps) => {
93110
</div>
94111
</div>
95112
<div className='flex items-center gap-2'>
96-
<div className='flex gap-1 font-mono text-xs'>
97-
{/* TODO: Get user avatar and display */}
98-
{members.map((member, index) => (
99-
<div
100-
className='grid size-8 place-items-center !overflow-clip rounded-full border-2 p-1 text-xs'
101-
style={{
102-
borderColor: member.color,
103-
}}
104-
key={index}
105-
>
106-
<span className='translate-x-[calc(-50%+12px)]'>{member.userId}</span>
107-
</div>
108-
))}
113+
<div className='flex flex-row gap-2'>
114+
<div className='flex gap-1 font-mono text-xs'>
115+
{/* TODO: Get user avatar and display */}
116+
{members.map((member, index) => (
117+
<div
118+
className='grid size-8 place-items-center !overflow-clip rounded-full border-2 p-1 text-xs'
119+
style={{
120+
borderColor: member.color,
121+
}}
122+
key={index}
123+
>
124+
<span className='translate-x-[calc(-50%+12px)]'>{member.userId}</span>
125+
</div>
126+
))}
127+
</div>
128+
</div>
129+
<CompleteDialog {...{ setCompleting: setIsCompleting, questionId, code, members }}>
130+
<Button size='sm' variant='destructive' disabled={!code} className='mx-4'>
131+
Complete question
132+
</Button>
133+
</CompleteDialog>
134+
<div className='flex flex-row gap-2'>
135+
<Button
136+
variant='outline'
137+
size='sm'
138+
className='group flex items-center !px-2 !py-1'
139+
onClick={onAIClick}
140+
>
141+
<Bot className='mr-1 size-4' />
142+
<span>AI Assistant</span>
143+
</Button>
144+
<Button
145+
variant='outline'
146+
size='sm'
147+
className='group flex items-center !px-2 !py-1'
148+
onClick={onPartnerClick}
149+
>
150+
<User className='mr-1 size-4' />
151+
<span>Chat</span>
152+
</Button>
109153
</div>
110-
<Button
111-
variant='outline'
112-
size='sm'
113-
className='group flex items-center !px-2 !py-1'
114-
onClick={onAIClick}
115-
>
116-
<Bot className='mr-1 size-4' />
117-
<span>AI Assistant</span>
118-
</Button>
119-
<Button
120-
variant='outline'
121-
size='sm'
122-
className='group flex items-center !px-2 !py-1'
123-
onClick={onPartnerClick}
124-
>
125-
<User className='mr-1 size-4' />
126-
<span>Chat</span>
127-
</Button>
128-
<Button variant='destructive' size='sm' className='group flex items-center !px-2 !py-1'>
129-
<ChevronLeftIcon className='transition-transform duration-200 ease-in-out group-hover:-translate-x-px' />
130-
<span>Disconnect</span>
131-
</Button>
132154
</div>
133155
</div>
134156
)}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
import { FC, PropsWithChildren, useCallback, useState } from 'react';
3+
4+
import { Button } from '@/components/ui/button';
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTrigger,
11+
} from '@/components/ui/dialog';
12+
import { IYjsUserState } from '@/types/collab-types';
13+
14+
type CompleteDialogProps = {
15+
questionId: number;
16+
code: string;
17+
members: Array<IYjsUserState['user']>;
18+
setCompleting: (state: string, resetId?: boolean) => void;
19+
};
20+
21+
export const CompleteDialog: FC<PropsWithChildren<CompleteDialogProps>> = ({
22+
children,
23+
setCompleting,
24+
}) => {
25+
const [isOpen, _setIsOpen] = useState(false);
26+
const setIsOpen = useCallback(
27+
(openState: boolean) => {
28+
_setIsOpen(openState);
29+
30+
if (openState) {
31+
setCompleting('pending');
32+
} else {
33+
setCompleting('', true);
34+
}
35+
},
36+
[isOpen]
37+
);
38+
39+
const { mutate: _m } = useMutation({
40+
mutationFn: async () => {},
41+
onSuccess: () => {
42+
setCompleting('success');
43+
// Navigate to home page
44+
},
45+
onError: () => {},
46+
});
47+
48+
return (
49+
<Dialog onOpenChange={setIsOpen} open={isOpen}>
50+
<DialogTrigger asChild>{children}</DialogTrigger>
51+
<DialogContent className='border-border'>
52+
<DialogHeader className='text-primary text-lg font-medium'>
53+
Are you sure you wish to mark this question as complete?
54+
</DialogHeader>
55+
<DialogFooter>
56+
<div className='flex w-full justify-between'>
57+
<Button
58+
variant='secondary'
59+
onClick={() => {
60+
setIsOpen(false);
61+
}}
62+
>
63+
Go Back
64+
</Button>
65+
<Button>Complete Question</Button>
66+
</div>
67+
</DialogFooter>
68+
</DialogContent>
69+
</Dialog>
70+
);
71+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog';
2+
3+
export const OtherUserCompletingDialog = () => {
4+
return (
5+
<Dialog open>
6+
<DialogContent className='text-primary border-border'>
7+
<DialogHeader className='text-lg font-medium'>
8+
The other user is marking this question attempt as complete. Please wait...
9+
</DialogHeader>
10+
<div className='bg-background absolute right-3 top-3 z-50 size-6' />
11+
</DialogContent>
12+
</Dialog>
13+
);
14+
};

frontend/src/lib/hooks/use-collab.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as Y from 'yjs';
99
import { extensions as baseExtensions, getLanguage } from '@/lib/editor/extensions';
1010
import { COLLAB_WS } from '@/services/api-clients';
1111
import { getUserId } from '@/services/user-service';
12+
import type { IYjsUserState } from '@/types/collab-types';
1213

1314
// credit: https://github.com/yjs/y-websocket
1415
const usercolors = [
@@ -26,8 +27,6 @@ const getRandomColor = () => {
2627
return usercolors[Math.floor(Math.random() * usercolors.length)];
2728
};
2829

29-
type IYjsUserState = { user: { name: string; userId: string; color: string; colorLight: string } };
30-
3130
// TODO: Test if collab logic works
3231
export const useCollab = (roomId: string) => {
3332
const [userId] = useState(getUserId());
@@ -36,10 +35,12 @@ export const useCollab = (roomId: string) => {
3635
const [extensions, setExtensions] = useState<Array<Extension>>(baseExtensions);
3736

3837
const [isLoading, setIsLoading] = useState(true);
38+
const [isCompleting, _setIsCompleting] = useState({ userId: '', state: '' });
3939
const [code, setCode] = useState('');
4040
const [language, _setLanguage] = useState<LanguageName>('python');
4141
const [members, _setMembers] = useState<Array<IYjsUserState['user']>>([]);
4242
const [cursorPosition, setCursorPosition] = useState({ lineNum: 1, colNum: 0 });
43+
4344
const setMembers = useCallback(_setMembers, [members]);
4445
const setLanguage = useCallback(
4546
(lang: LanguageName) => {
@@ -49,6 +50,13 @@ export const useCollab = (roomId: string) => {
4950
},
5051
[sharedDocRef]
5152
);
53+
const setIsCompleting = useCallback(
54+
(completingState: string, resetId?: boolean) => {
55+
_setIsCompleting((s) => ({ ...s, state: completingState }));
56+
sharedDocRef?.set('complete', { userId: resetId ? '' : userId, state: completingState });
57+
},
58+
[sharedDocRef]
59+
);
5260

5361
const langExtension = useMemo(() => {
5462
return getLanguage(language);
@@ -116,6 +124,9 @@ export const useCollab = (roomId: string) => {
116124
const lang = yState.get('language') as LanguageName;
117125
_setLanguage(lang);
118126
console.log('Language changed to: ' + lang);
127+
} else if (event.keysChanged.has('complete')) {
128+
console.log(yState.get('complete'));
129+
_setIsCompleting(yState.get('complete') as typeof isCompleting);
119130
}
120131
});
121132
_setLanguage((yState.get('language') as LanguageName) ?? 'python');
@@ -139,6 +150,9 @@ export const useCollab = (roomId: string) => {
139150
setCode,
140151
members,
141152
isLoading,
153+
userId,
154+
isCompleting,
155+
setIsCompleting,
142156
cursorPosition,
143157
};
144158
};

frontend/src/routes/interview/[room]/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const InterviewRoom = () => {
5454
</Card>
5555
<div className='flex w-full'>
5656
<Editor
57+
questionId={questionId}
5758
room={roomId as string}
5859
onAIClick={handleAIClick}
5960
onPartnerClick={handlePartnerClick}

frontend/src/routes/match/logic.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { defer, LoaderFunctionArgs } from 'react-router-dom';
66
import { z } from 'zod';
77

88
import { requestMatch } from '@/services/match-service';
9-
import { fetchTopics } from '@/services/question-service';
9+
import { fetchDifficulties, fetchTopics } from '@/services/question-service';
1010
import { getUserId } from '@/services/user-service';
1111

1212
export interface MatchFormData {
@@ -20,11 +20,19 @@ const getTopicsQueryConfig = () =>
2020
queryFn: async () => fetchTopics(),
2121
});
2222

23+
const getDifficultiesQueryConfig = () => {
24+
return queryOptions({
25+
queryKey: ['difficulties'],
26+
queryFn: async () => fetchDifficulties(),
27+
});
28+
};
29+
2330
export const loader =
2431
(queryClient: QueryClient) =>
2532
async ({ params: _ }: LoaderFunctionArgs) => {
2633
return defer({
2734
topics: queryClient.ensureQueryData(getTopicsQueryConfig()),
35+
difficulties: queryClient.ensureQueryData(getDifficultiesQueryConfig()),
2836
});
2937
};
3038

frontend/src/routes/match/main.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,23 @@ import { ScrollArea } from '@/components/ui/scroll-area';
77
import { MatchForm } from './match-form';
88

99
export const Match = observer(() => {
10-
const { topics } = useLoaderData() as { topics: Promise<Array<string>> };
10+
const { topics, difficulties } = useLoaderData() as {
11+
topics: Promise<{ topics: Array<string> }>;
12+
difficulties: Promise<{ difficulties: Array<string> }>;
13+
};
1114

1215
return (
1316
<ScrollArea className='flex size-full py-8'>
1417
<Suspense fallback={<div>Loading topics...</div>}>
15-
<Await resolve={topics}>
16-
{(resolvedTopics) => <MatchForm topics={resolvedTopics.topics} />}
18+
<Await resolve={Promise.all([topics, difficulties])}>
19+
{([resolvedTopics, resolvedDifficulties]) => {
20+
return (
21+
<MatchForm
22+
topics={resolvedTopics.topics}
23+
difficulties={resolvedDifficulties.difficulties}
24+
/>
25+
);
26+
}}
1727
</Await>
1828
</Suspense>
1929
</ScrollArea>

0 commit comments

Comments
 (0)