Skip to content
Merged
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
146 changes: 128 additions & 18 deletions bun.lock

Large diffs are not rendered by default.

651 changes: 651 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
"@mediapipe/face_mesh": "^0.4.1633559619",
"@monaco-editor/react": "^4.6.0",
"@prisma/client": "^6.7.0",
"@radix-ui/react-accordion": "^1.2.10",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.11",
"@supabase/supabase-js": "^2.48.1",
"@tensorflow-models/face-landmarks-detection": "^1.0.6",
"@tensorflow/tfjs": "^4.22.0",
Expand All @@ -33,6 +35,8 @@
"face-api.js": "^0.22.2",
"firebase": "^11.3.1",
"framer-motion": "^12.0.5",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.473.0",
"next": "^14.2.24",
"next-themes": "^0.4.4",
Expand Down
84 changes: 84 additions & 0 deletions src/app/api/save/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import prisma from "@/db/prisma";
import { GEMINI_1_5_FLASH } from "@/lib/utils/ai";
import { generateObject } from "ai";
import { z } from "zod";

const InterviewEvaluationSchema = z.object({
scores: z.object({
communicationSkills: z.number().min(0).max(10),
technicalKnowledge: z.number().min(0).max(10),
problemSolvingAbility: z.number().min(0).max(10),
behavioralCompetence: z.number().min(0).max(10),
overallImpression: z.number().min(0).max(10),
}),

feedback: z.object({
communicationFeedback: z.string().min(50),
technicalFeedback: z.string().min(50),
problemSolvingFeedback: z.string().min(50),
behavioralFeedback: z.string().min(50),
overallFeedback: z.string().min(100),
}),

highlights: z.object({
topStrengths: z.array(z.string()).min(2).max(5),
improvementAreas: z.array(z.string()).min(2).max(5),
}),

recommendations: z.array(z.string()).min(3).max(7),

keyMoments: z
.array(
z.object({
question: z.string(),
responseEvaluation: z.string().min(50),
improvementSuggestion: z.string().min(30),
})
)
.min(2)
.max(5),
});

export async function POST(req: Request) {
try {
const prompt = await prisma.prompts.findFirst({
where: {
name: "end-interview",
},
});
if (!prompt) {
return new Response(
JSON.stringify({
success: false,
data: null,
}),
{ status: 500 }
);
}
const { conversation } = await req.json();
const response = await generateObject({
model: GEMINI_1_5_FLASH,
messages: conversation,
system: prompt.prompt,
schema: InterviewEvaluationSchema,
});
if (!response) {
throw new Error("Failed to generate response");
}
return new Response(
JSON.stringify({
success: true,
data: response,
}),
{ status: 200 }
);
} catch {
return new Response(
JSON.stringify({
success: false,
data: null,
}),
{ status: 500 }
);
}
}
10 changes: 4 additions & 6 deletions src/app/company/[style]/[meet]/_components/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ export function Conversation({
: "bg-yellow-100 text-yellow-800";

return (
<div className="max-w-md mx-auto mt-2 p-6 rounded-2xl shadow-xl bg-white space-y-6 border border-gray-200">
<h2 className="text-2xl font-semibold text-center text-gray-800">
Voice Conversation
</h2>
<div className="max-w-md mx-auto mt-6 p-6 rounded-2xl shadow-xl bg-white space-y-6 border border-gray-200">


<div className="text-center">
<p
Expand All @@ -24,7 +22,7 @@ export function Conversation({
Status: {conversation.status}
</p>
<p className="mt-2 text-sm text-gray-600">
Agent is currently{" "}
Inteviewer is currently{" "}
<span className="font-medium">
{conversation.isSpeaking ? "speaking" : "listening"}
</span>
Expand All @@ -36,7 +34,7 @@ export function Conversation({
<button
onClick={stopConversation}
disabled={conversation.status !== "connected"}
className="px-6 py-2 rounded-lg bg-red-600 text-white font-semibold shadow-md transition hover:bg-red-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
className="px-3 py-1 rounded-lg bg-red-600 text-white font-semibold shadow-md transition hover:bg-red-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Stop Conversation
</button>
Expand Down
12 changes: 5 additions & 7 deletions src/app/company/[style]/[meet]/_components/LeftPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ResizablePanel,
ResizablePanelGroup,
ResizableHandle,
} from "../../../../../components/ui/resizable";
} from "@/components/ui/resizable";
import { CircleUserRound } from "lucide-react";
import { usePathname } from "next/navigation";
import { interviewStore } from "@/lib/utils/interviewStore";
Expand All @@ -16,9 +16,7 @@ interface LeftPanelProps {

export default function LeftPanel({ code, setCode }: LeftPanelProps) {
const pathname = usePathname();
const { subtitles, aiSpeaking } = interviewStore();

const showSubtitle = subtitles && aiSpeaking;
const { subtitles } = interviewStore();

return (
<ResizablePanel defaultSize={75} className="flex flex-col h-full">
Expand Down Expand Up @@ -59,7 +57,7 @@ export default function LeftPanel({ code, setCode }: LeftPanelProps) {
) : (
<div
className={`h-full flex flex-col gap-6 items-center p-6 text-center backdrop-blur-sm transition-all duration-300 ease-in-out ${
showSubtitle ? "justify-between" : "justify-center"
"justify-center"
}`}
>
<div className="flex flex-col gap-4 items-center">
Expand All @@ -69,8 +67,8 @@ export default function LeftPanel({ code, setCode }: LeftPanelProps) {
</h1>
</div>

{showSubtitle && (
<div className="bg-white/90 text-white w-full p-4 rounded-xl shadow-xl max-w-3xl">
{subtitles && (
<div className="bg-white/90 text-white w-full p-4 rounded-xl shadow-xl">
<p className="text-base font-medium leading-relaxed whitespace-pre-wrap text-gray-800">
{subtitles}
</p>
Expand Down
5 changes: 3 additions & 2 deletions src/app/company/[style]/[meet]/_components/RightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ export default function RightPanel({
>
<div className="w-full flex justify-center items-center">
<Button
onClick={() => {
onClick={async () => {
if (mediaStream) {
mediaStream.getTracks().forEach((track) => track.stop());
}
router.push("/end");
await stopConversation();
router.replace("/end");
}}
>
End Meeting
Expand Down
27 changes: 22 additions & 5 deletions src/app/company/[style]/[meet]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,34 @@ export default function Home() {
const [seconds, setSeconds] = useState(0);
const mins = interviewStore((state) => state.minutes);
const secs = interviewStore((state) => state.seconds);

const setConversation = interviewStore((state) => state.setConversation);
const { setSubtitles } = interviewStore();
const candidate = generalStore((state) => state.candidate);
if (!candidate) {
console.log("Please Login to start the meet.");
}
const conversation = useConversation({
onConnect: () => console.log("Connected"),
onDisconnect: (error) => console.log("Disconnected", error),
onDisconnect: (error) => {
if (error) {
console.error("Disconnected:", error);
} else {
console.log("Disconnected");
}
},
onMessage: (message: { message: string; source: string }) => {
console.log("Message received:", message);
setSubtitles(message.message);
if (message.source === "ai") {
setSubtitles(message.message);
} else {
setSubtitles(null);
}
setConversation([
...interviewStore.getState().conversation,
{
role: message.source === "ai" ? "assistant" : "user",
content: message.message,
},
]);
},
onError: (error) => console.error("Error:", error),
});
Expand All @@ -49,7 +65,8 @@ export default function Home() {

useEffect(() => {
startConversation();
}, [startConversation]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const stopConversation = useCallback(async () => {
await conversation.endSession();
Expand Down
95 changes: 43 additions & 52 deletions src/app/end/page.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,54 @@
"use client";

import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { EvaluationDashboard } from "@/components/end/evaluation-dashboard";
import { interviewStore } from "@/lib/utils/interviewStore";
import { toaster } from "@/components/toast";
const quotes = [
"The only way to do great work is to love what you do. - Steve Jobs",
"Success is not final, failure is not fatal: it is the courage to continue that counts. - Winston Churchill",
"The future belongs to those who believe in the beauty of their dreams. - Eleanor Roosevelt",
"Believe you can and you're halfway there. - Theodore Roosevelt",
"The best way to predict the future is to create it. - Peter Drucker",
];
export default function LoadingPage() {
const [currentQuote, setCurrentQuote] = useState(0);
const endMeeting = interviewStore((state) => state.endInterview);
useEffect(() => {
const interval = setInterval(() => {
setCurrentQuote((prev) => (prev + 1) % quotes.length);
}, 5000);
import { useEffect, useState } from "react";
export default function Home() {
const [evaluation, setEvaluation] = useState(null);
const [error, setError] = useState("");
const conversation = interviewStore((state) => state.conversation);

return () => clearInterval(interval);
}, []);
useEffect(() => {
const endTheMeeting = async () => {
const response = await endMeeting();
if (!response) {
toaster(
"Error generating the evaluation report. Please try again later."
);
async function fetchEvaluation() {
const response = await fetch("/api/save", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
conversation,
}),
});
if (!response.ok) {
throw new Error("Failed to fetch evaluation");
}
const data = await response.json();
console.log(data);
if (data.success) {
setEvaluation(data.data.object);
} else {
window.open(response, "_blank");
setError("Failed to fetch evaluation");
}
}
fetchEvaluation()
.then(() => {
console.log("Evaluation fetched successfully");
})
.catch((err) => {
console.error(err);
setError("Failed to fetch evaluation");
});
return () => {
setEvaluation(null);
setError("");
};
endTheMeeting();
}, [endMeeting]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div className="min-h-screen w-full bg-gradient-custom flex flex-col items-center justify-center p-4">
<motion.div
className="text-white text-6xl mb-8"
animate={{ rotate: 360 }}
transition={{
duration: 2,
repeat: Number.POSITIVE_INFINITY,
ease: "linear",
}}
>
⚙️
</motion.div>
<motion.div
className="text-center max-w-md"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
key={currentQuote}
>
<p className="text-white text-lg italic">{`"${quotes[currentQuote]}"`}</p>
</motion.div>
</div>
<main className="min-h-screen bg-gradient-to-r from-rose-300 via-amber-300 to-amber-200">
<div className="container mx-auto py-8 px-4">
<EvaluationDashboard evaluation={evaluation} error={error} />
</div>
</main>
);
}
Loading