diff --git a/client/public/images/coverImage.png b/client/public/images/coverImage.png new file mode 100644 index 0000000..edcd2f7 Binary files /dev/null and b/client/public/images/coverImage.png differ diff --git a/client/src/app/components/loadingSubmission.tsx b/client/src/app/components/loadingSubmission.tsx new file mode 100644 index 0000000..07676ec --- /dev/null +++ b/client/src/app/components/loadingSubmission.tsx @@ -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 ( +
+ } + size="large" + /> +

This may take several minutes, hold tight!

+
+ ); +} \ No newline at end of file diff --git a/client/src/app/interfaces.ts b/client/src/app/interfaces.ts index f5e83be..db7aba0 100644 --- a/client/src/app/interfaces.ts +++ b/client/src/app/interfaces.ts @@ -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', diff --git a/client/src/app/start/feedback/page.tsx b/client/src/app/start/feedback/page.tsx index d81771c..2193b42 100644 --- a/client/src/app/start/feedback/page.tsx +++ b/client/src/app/start/feedback/page.tsx @@ -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 ( -
-
+ +
+
+

{question}

+
+
+
+

"{answer}"

+
+
+
+
+

Feedback:

+

{feedback}

+
+
+

Score:

+

{score}

+
+
+
+
+
+
+ ) +} + +export default function FeedbackPage() { + const [feedback, setFeedback] = useState(null); + const [loading, setLoading] = useState(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 ? : feedback ? ( +
+

Feedback

+
+ {feedback.map((feedback) => { + return ( + + ) + })} +
- ); + ) : <> No Feedback ; } diff --git a/client/src/app/start/history/page.tsx b/client/src/app/start/history/page.tsx index 119cb98..939eaa5 100644 --- a/client/src/app/start/history/page.tsx +++ b/client/src/app/start/history/page.tsx @@ -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([]); + const [loading, setLoading] = useState(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 ( -
-
+
+

History

-
- {previousInterviews.length === 0 ?

You have no history of interviews, schedule one today!

: previousInterviews.map((interview) => { - return
{interview}
- })} -
+ {!loading ? ( +
+ {previousInterviews.length === 0 ? ( +

You have no history of interviews, schedule one today!

+ ) : ( +
+ + + + + + + + + + + + {/* 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 ( + { + router.push(`/start/feedback?id=${interview._id}`); + }} + > + + + + + + + ); + } + })} + +
+ Position + +

Duration

+
+ + +
+
+ Questions + + Score + + Date +
+ Software Engineer + + 00:30 + + {feedback.length} + 90 + ? 'text-[#389e0d]' + : score > 75 + ? 'text-[#73d13d]' + : score > 50 + ? 'text-[#ffa940]' + : 'text-[#f5222d]' + }`} + > + {getAverageScore(feedback)} + + {extractDateFromTimestamp(interview.created_at)} +
+
+ )} +
+ ) : ( +
+ } + size="large" + /> +
+ )}
); } diff --git a/client/src/app/start/interview/page.tsx b/client/src/app/start/interview/page.tsx index 842f0b4..54aa123 100644 --- a/client/src/app/start/interview/page.tsx +++ b/client/src/app/start/interview/page.tsx @@ -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', @@ -49,7 +53,6 @@ export default function Interview() { withCredentials: true, }, ); - console.log('Success', evaluations, resp); router.push('/start/feedback'); }; if (evaluations.length == numQs) saveToMongo(); @@ -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]); @@ -118,10 +127,6 @@ export default function Interview() { const [muted, setMuted] = useState(false); - function handleData(recordedBlob: any) { - console.log('chunk of real-time data is: ', recordedBlob); - } - useEffect(() => { const questions = GetQuestions(profession ? profession : undefined, numQs); setQuestions(questions); @@ -191,7 +196,7 @@ export default function Interview() { restartQuestion(); }; - return ( + return loading ? : (
diff --git a/client/src/app/start/layout.tsx b/client/src/app/start/layout.tsx index 96a7df7..60bc7ee 100644 --- a/client/src/app/start/layout.tsx +++ b/client/src/app/start/layout.tsx @@ -165,7 +165,7 @@ export default function RegularLayout({ style={{ background: 'white' }} >
-
+
}>{children}
diff --git a/client/src/app/styles/loading.css b/client/src/app/styles/loading.css index f0cabe2..a113966 100644 --- a/client/src/app/styles/loading.css +++ b/client/src/app/styles/loading.css @@ -3,4 +3,6 @@ justify-content: center; align-items: center; min-height: 100vh; + flex-direction: column; + gap: 1rem; } \ No newline at end of file diff --git a/server/routes/mongo.ts b/server/routes/mongo.ts index 90cf0c6..f74e9a7 100644 --- a/server/routes/mongo.ts +++ b/server/routes/mongo.ts @@ -5,6 +5,7 @@ import { Schema } from 'mongoose'; //utils import { saveToMongo } from '../utils/saveToMongo'; import { getLatestInterview } from '../utils/getLatestInterview'; +import { getAllInterviews } from '../utils/getAllInterviews'; //configs const router = Router(); @@ -23,9 +24,26 @@ router.post('/save-interview', async (req, res) => { router.get('/get-interview', async (req, res) => { const userId = (req.user as { _id: Schema.Types.ObjectId })._id; + const interviewID = req.query.interviewID as string | null; + + try { + if (!interviewID) { + const interview = await getLatestInterview(userId, null); + res.status(200).send(interview); + } else { + const interview = await getLatestInterview(userId, interviewID); + res.status(200).send(interview); + } + } catch (err) { + res.status(400).send({ error: `❌ Mongo error: ${err}` }); + } +}); + +router.get('/get-all-interviews', async (req, res) => { + const userId = (req.user as {_id: Schema.Types.ObjectId })._id; try { - const interview = await getLatestInterview(userId); - res.status(200).send(interview); + const interviews = await getAllInterviews(userId); + res.status(200).send(interviews); } catch (err) { res.status(400).send({ error: `❌ Mongo error: ${err}` }); } diff --git a/server/utils/getAllInterviews.ts b/server/utils/getAllInterviews.ts new file mode 100644 index 0000000..b7c22eb --- /dev/null +++ b/server/utils/getAllInterviews.ts @@ -0,0 +1,21 @@ +import interviewModel from '../Models/interviewModel'; +import qa_feedbackModel from '../Models/qa_feedbackModel'; + +export async function getAllInterviews(userId) { + const interviews = await interviewModel + .find({ user_id: userId }); + + // This means that map would return an array of promises of the results we want. + const feedback = interviews.map(async (interview) => { + const feedbacks = await qa_feedbackModel.find({ + interview_id: interview._id, + }); + return { + interview: interview, + feedback: feedbacks + } + }) + + // We can then use Promise.all to get the result when all the promises return. + return Promise.all(feedback); +} \ No newline at end of file diff --git a/server/utils/getLatestInterview.ts b/server/utils/getLatestInterview.ts index 009a5ea..820b083 100644 --- a/server/utils/getLatestInterview.ts +++ b/server/utils/getLatestInterview.ts @@ -1,14 +1,21 @@ -import mongoose from 'mongoose'; import interviewModel from '../Models/interviewModel'; import qa_feedbackModel from '../Models/qa_feedbackModel'; -export async function getLatestInterview(userId) { - const interview = await interviewModel - .findOne({ user_id: userId }) - .sort({ _id: -1 }); +export async function getLatestInterview(userId, interviewID: string | null) { + if(interviewID !== null){ + const interview = await interviewModel.findOne({user_id: userId, _id: interviewID}); + const feedbacks = await qa_feedbackModel.find({ + interview_id: interview._id, + }) + return feedbacks; + } else { + const interview = await interviewModel + .findOne({ user_id: userId }) + .sort({ _id: -1 }); - const feedbacks = await qa_feedbackModel.find({ - interview_id: interview._id, - }); - return feedbacks; + const feedbacks = await qa_feedbackModel.find({ + interview_id: interview._id, + }); + return feedbacks; + } } diff --git a/server/yarn.lock b/server/yarn.lock index 78e3370..760d1f8 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -464,13 +464,6 @@ crypt@0.0.2: resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" @@ -485,12 +478,19 @@ debug@4.x: dependencies: ms "2.1.2" +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -depd@~2.0.0, depd@2.0.0: +depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -735,7 +735,7 @@ ignore-by-default@^1.0.1: resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== -inherits@^2.0.3, inherits@~2.0.3, inherits@2.0.4: +inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -926,22 +926,17 @@ mquery@5.0.0: dependencies: debug "4.x" -ms@^2.0.0, ms@2.0.0: +ms@2.0.0, ms@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -1019,13 +1014,6 @@ object-inspect@^1.9.0: resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" - integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== - dependencies: - ee-first "1.1.1" - on-finished@2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" @@ -1033,6 +1021,13 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + on-headers@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" @@ -1190,17 +1185,7 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -safe-buffer@~5.1.0: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.1.2: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -1389,7 +1374,7 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^5.2.2, typescript@>=2.7: +typescript@^5.2.2: version "5.2.2" resolved "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== @@ -1411,7 +1396,7 @@ undefsafe@^2.0.5: resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -unpipe@~1.0.0, unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -1421,7 +1406,7 @@ util-deprecate@~1.0.1: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -utils-merge@^1.0.1, utils-merge@1.0.1, utils-merge@1.x.x: +utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==