Skip to content
Draft
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
Binary file added client/public/images/coverImage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions client/src/app/components/loadingSubmission.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { Spin } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';

export default function LoadingSubmission() {
// You can add any UI inside Loading, including a Skeleton.
return (
<div className="Loading">
<Spin
indicator={<LoadingOutlined style={{ fontSize: 50 }} />}
size="large"
/>
<p>This may take several minutes, hold tight!</p>
</div>
);
}
12 changes: 12 additions & 0 deletions client/src/app/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ export interface User {
_id: string;
}

export interface Feedback {
answer: string;
created_at: string;
explanation: string;
feedback: string;
interview_id: string;
question: string;
score: number;
__v: number;
_id: string;
}

export enum ProfessionType {
ALL = 'all',
SOFTWARE_ENGINEER = 'Software Engineer',
Expand Down
78 changes: 73 additions & 5 deletions client/src/app/start/feedback/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,80 @@
'use client';
import React from 'react';
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Feedback } from '../../interfaces';
import LoadingSubmission from '../../components/loadingSubmission';
import { Card, Tooltip } from 'antd';
import { useSearchParams } from 'next/navigation'
import { InfoCircleOutlined } from '@ant-design/icons';

function FeedbackCard( props: Feedback ) {
const { answer, explanation, feedback, question, score } = props;

export default function Feedback() {
return (
<div className="lg:max-w-screen-lg lg:mx-auto flex flex-col w-full mt-8 gap-16 lg:gap-36">
<div className="flex flex-col gap-4 mx-auto md:mx-auto px-4 md:px-0 md:w-11/12 w-10/12">
<Card className='w-full'>
<div className='flex flex-col gap-4'>
<div className='flex gap-1'>
<p className='text-xl font-bold'>{question} <Tooltip title={explanation}><InfoCircleOutlined className='text-lg my-auto' /></Tooltip></p>
</div>
<div className='grid grid-cols-2 gap-4'>
<div className='col-span-1'>
<p className='text-justify w-full text-[#626262]'>"{answer}"</p>
</div>
<div className='col-span-1'>
<div className='w-full rounded-md bg-[#52c41a] flex flex-col p-3 text-white'>
<div className='flex flex-col'>
<p className='font-bold'>Feedback:</p>
<p>{feedback}</p>
</div>
<div className='flex gap-1 font-bold'>
<p>Score: </p>
<p>{score}</p>
</div>
</div>
</div>
</div>
</div>
</Card>
)
}

export default function FeedbackPage() {
const [feedback, setFeedback] = useState<Feedback[] | null>(null);
const [loading, setLoading] = useState<boolean>(true);

const searchParams = useSearchParams();
const interviewID = searchParams.get('id');

useEffect(() => {
const getLatestInterview = async () => {
try {
const resp = await axios.get(
`http://localhost:4000/mongo/get-interview${interviewID ? `?interviewID=${interviewID}` : ''}`,
{
withCredentials: true,
},
);
setFeedback(resp.data);
} catch (error) {
console.log(error)
}
}
setLoading(true);
getLatestInterview().then(() => setTimeout(() => setLoading(false), 500));
}, [])

return loading ? <LoadingSubmission /> : feedback ? (
<div className="lg:max-w-screen-lg h-full lg:mx-auto flex flex-col w-full mt-8 gap-16 lg:gap-36">
<div className="flex flex-col gap-5 mx-auto md:mx-auto px-4 md:px-0 md:w-11/12 w-10/12">
<p className="text-2xl font-bold">Feedback</p>
<div className='flex flex-col gap-3 w-full overflow-y-auto'>
{feedback.map((feedback) => {
return (
<FeedbackCard {...feedback} />
)
})}
</div>
</div>
</div>
);
) : <> No Feedback </>;
}
150 changes: 141 additions & 9 deletions client/src/app/start/history/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,151 @@
'use client';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import {
CaretDownOutlined,
CaretUpOutlined,
LoadingOutlined,
} from '@ant-design/icons';
import { Spin } from 'antd';
import { useRouter } from 'next/navigation';

export default function History() {
const [previousInterviews, setPreviousInterviews] = useState([]);
const [previousInterviews, setPreviousInterviews] = useState<any[]>([]);
const [loading, setLoading] = useState<Boolean>(true);

const router = useRouter();

function getAverageScore(feedbacks: any) {
const length = feedbacks.length;
const totalScore = feedbacks.reduce(
(accumulator: number, feedback: any) => accumulator + feedback.score,
0,
);
return Math.floor(totalScore / length);
}

function extractDateFromTimestamp(timestamp: any) {
const dateObject = new Date(timestamp);
const year = dateObject.getFullYear();
const month = String(dateObject.getMonth() + 1).padStart(2, '0'); // Months are zero-indexed
const day = String(dateObject.getDate()).padStart(2, '0');

return `${month}-${day}-${year}`;
}

useEffect(() => {
const getAllUserInterviews = async () => {
try {
const resp = await axios.get(
'http://localhost:4000/mongo/get-all-interviews',
{
withCredentials: true,
},
);
setPreviousInterviews(resp.data);
} catch (error) {
console.log('error: ' + error);
}
};
getAllUserInterviews().then(() => setTimeout(() => setLoading(false), 500));
}, []);

return (
<div className="lg:max-w-screen-lg lg:mx-auto flex flex-col w-full mt-8 gap-8 lg:gap-12">
<div className="flex flex-col gap-4 mx-auto md:mx-auto px-4 md:px-0 md:w-11/12 w-10/12">
<div className="lg:max-w-screen-lg lg:mx-auto flex flex-col w-full mt-8 gap-5">
<div className="flex flex-col gap-4 mx-auto md:mx-auto px-4 md:px-0 md:w-full w-10/12">
<p className="text-2xl font-bold">History</p>
</div>
<div className="flex flex-col gap-2 mx-auto px-4 md:px-0 md:w-11/12 w-10/12">
{previousInterviews.length === 0 ? <p>You have no history of interviews, schedule one today!</p> : previousInterviews.map((interview) => {
return <div>{interview}</div>
})}
</div>
{!loading ? (
<div className="flex flex-col gap-2 mx-auto px-4 md:px-0 md:w-full w-10/12">
{previousInterviews.length === 0 ? (
<p>You have no history of interviews, schedule one today!</p>
) : (
<div className="shadow-md overflow-hidden my-4">
<table className="border-collapse table-fixed w-full text-sm rounded-t-md">
<thead className="text-base">
<tr className="bg-[#F5F5F5]">
<th className="border-b p-4 pl-8 pt-3 pb-3 text-[#595959] text-center font-bold">
Position
</th>
<th className="border-b p-4 pt-3 pb-3 text-[#3772FF] text-center font-bold flex justify-center gap-1">
<p>Duration</p>
<div className="flex flex-col text-xs">
<CaretUpOutlined />
<CaretDownOutlined />
</div>
</th>
<th className="border-b p-4 pr-8 pt-3 pb-3 text-[#595959] text-center font-bold">
Questions
</th>
<th className="border-b p-4 pr-8 pt-3 pb-3 text-[#595959] text-center font-bold">
Score
</th>
<th className="border-b p-4 pr-8 pt-3 pb-3 text-[#595959] text-center font-bold">
Date
</th>
</tr>
</thead>
<tbody className="bg-white">
{/* TO DO: fix this type, make interface */}
{previousInterviews
.reverse()
.map((previousInterview: any) => {
const { interview, feedback } = previousInterview;
const score = getAverageScore(feedback);

if (feedback.length <= 0) {
return;
} else {
return (
<tr
className="hover:bg-[#fafafa] cursor-pointer"
key={interview._id}
onClick={() => {
router.push(`/start/feedback?id=${interview._id}`);
}}
>
<td className="border-b border-slate-100 p-4 pl-8 text-black font-semibold text-center">
Software Engineer
</td>
<td className="border-b border-slate-100 p-4 text-slate-500 text-center">
00:30
</td>
<td className="border-b border-slate-100 p-4 pr-8 text-slate-500 text-center">
{feedback.length}
</td>
<td
className={`border-b border-slate-100 p-4 pr-8 text-center ${
score > 90
? 'text-[#389e0d]'
: score > 75
? 'text-[#73d13d]'
: score > 50
? 'text-[#ffa940]'
: 'text-[#f5222d]'
}`}
>
{getAverageScore(feedback)}
</td>
<td className="border-b border-slate-100 p-4 pr-8 text-slate-500 text-center">
{extractDateFromTimestamp(interview.created_at)}
</td>
</tr>
);
}
})}
</tbody>
</table>
</div>
)}
</div>
) : (
<div className="h-[80vh] flex justify-center items-center">
<Spin
indicator={<LoadingOutlined style={{ fontSize: 50 }} />}
size="large"
/>
</div>
)}
</div>
);
}
21 changes: 13 additions & 8 deletions client/src/app/start/interview/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
'use client';
import React, { useEffect, useState } from 'react';
import GetQuestions from '@/app/utils/get-questions';
import GetQuestions from '../../utils/get-questions';
import { useRouter, useSearchParams } from 'next/navigation';
import getProfessionType from '@/app/utils/get-profession-type';
import getProfessionType from '../../utils/get-profession-type';
import Card from 'antd/es/card/Card';
import { AudioOutlined, AudioMutedOutlined } from '@ant-design/icons';
import { Button, Progress, Modal } from 'antd';
import { ReactMic } from 'react-mic';
import LoadingSubmission from '../../components/loadingSubmission';
import axios from 'axios';

export default function Interview() {
const params = useSearchParams();
const router = useRouter();

const [loading, setLoading] = useState(false);

const profession = getProfessionType(params.get('profession'));
const numQs = parseInt(
params.get('numberQs') ? String(params.get('numberQs')) : '0',
Expand Down Expand Up @@ -49,7 +53,6 @@ export default function Interview() {
withCredentials: true,
},
);
console.log('Success', evaluations, resp);
router.push('/start/feedback');
};
if (evaluations.length == numQs) saveToMongo();
Expand All @@ -60,6 +63,12 @@ export default function Interview() {
const formData = new FormData();

let currIdx = blobs.length - 1;

// means this is the last question, for good UI we want to execute the loading to hide the delay of this response evaluation and saving to mongodb
if(blobs.length === numQs){
setLoading(true);
}

formData.append('question', questions[currIdx] as string);
formData.append('profession', profession as string);
formData.append('audio', blobs[currIdx]);
Expand Down Expand Up @@ -118,10 +127,6 @@ export default function Interview() {

const [muted, setMuted] = useState<boolean>(false);

function handleData(recordedBlob: any) {
console.log('chunk of real-time data is: ', recordedBlob);
}

useEffect(() => {
const questions = GetQuestions(profession ? profession : undefined, numQs);
setQuestions(questions);
Expand Down Expand Up @@ -191,7 +196,7 @@ export default function Interview() {
restartQuestion();
};

return (
return loading ? <LoadingSubmission /> : (
<div className="lg:max-w-screen-lg lg:mx-auto lg:justify-normal flex flex-col w-full mt-8 gap-16 lg:gap-36 h-full justify-center">
<div className='flex'>
<div className='w-full flex flex-col gap-2'>
Expand Down
2 changes: 1 addition & 1 deletion client/src/app/start/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export default function RegularLayout({
style={{ background: 'white' }}
>
<div className="h-full bg-[#1677ff] flex justify-center items-center">
<div className='h-[95%] w-[95%] bg-white rounded-xl drop-shadow'>
<div className='h-[95%] w-[95%] bg-white rounded-xl drop-shadow overflow-y-auto'>
<Suspense fallback={<Loading />}>{children}</Suspense>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/styles/loading.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
justify-content: center;
align-items: center;
min-height: 100vh;
flex-direction: column;
gap: 1rem;
}
Loading