Introduction
In this tutorial we'll build a web application called AI Coding Interviewer (e.g., PrepAlly
) that helps candidates prepare for coding interviews.
Well, okey, but why? π€
Current tools offer practice but fall short of providing the interactive, AI-driven insights candidates need to feel truly prepared. Interviews are still notoriously stressful, leaving candidates feeling unprepared despite available resources. PrepAlly changes that by delivering real-time feedback and personalized insights, empowering candidates with the confidence and readiness to ace their interviews.
you: it seems like at the end of the tutorial, we'll have a real startup, right? π
me: exactly, let's cook something people want! π¦
To build the AI Coding Interviewer, we'll use the following technologies: AI/ML API, React, Next.js, Tailwind CSS, Clerk Auth, Vercel, and Judge0 from RapidAPI. They all are nice to get started with MVP.
how everything is connected? π€
Both frontend and backend will be built using React
, and Next.js
, a React
framework that enables server-side rendering and static site generation. We'll use Tailwind CSS
to properly style the application and make it look good to convince the users to use our product. Also for API routes we'll use Next API routes. No need to setup a separate server. π₯
Nice strategy, right? π
We'll use Clerk
to handle user authentication and authorization in the application. This will allow us to create a secure and user-friendly experience for our users. Setting up Clerk is super easy & peasy. π
it's a must-have feature for any web application. π‘οΈ
We'll also use Judge0
from RapidAPI to provide online code execution capabilities in the application. This will allow users to run their code and get real-time feedback on their coding skills. It's free *almost π
it's a game-changer for coding interview preparation. π―
Hey, wait! What about AI/ML API
? π€
The most exciting part of this project is the integration with AI/ML API, a platform that provides access to over 200 state-of-the-art AI models. We'll use AI/ML API to power the AI-driven insights in the application, providing candidates with personalized feedback and recommendations to improve their coding skills. We'll be using two models from AI/ML API:
-
GPT-4o
: for delivering real-time feedback and personalized insights. π€ Learn more -
Deepgram Aura
: the first text-to-speech (TTS) AI model designed for real-time, conversational AI agents and applications. It delivers human-like voice quality with unparalleled speed and efficiency, making it a game-changer for building responsive, high-throughput voice AI experiences. π Learn more
sis (bro), ai/ml api uptime is 99.99% and it's super fast! π¨
Finally, we'll deploy the application to Vercel
, a cloud platform for deploying and hosting web applications. This will make the application accessible to users worldwide and ensure a smooth user experience. You can also connect your custom domain to Vercel. π
It'll be a really comprehensive tutorial that covers everything from setting up the project to hyping it up on ProductHunt and X (prev. Twitter). π
it'll be pretty fun tho! π
So, let's get started! π
AI/ML API
AI/ML API is a game-changing platform for developers and SaaS entrepreneurs looking to integrate cutting-edge AI capabilities into their products. It offers a single point of access to over 200 state-of-the-art AI models, covering everything from NLP to computer vision.
Key Features for Developers:
- Extensive Model Library: 200+ pre-trained models for rapid prototyping and deployment. π
- Customization Options: Fine-tune models to fit your specific use case. π―
- Developer-Friendly Integration: RESTful APIs and SDKs for seamless incorporation into your stack. π οΈ
- Serverless Architecture: Focus on coding, not infrastructure management. βοΈ
Get Started for FREE ($0 US dollars): Click me, let's Cook! π§βπ³
A$AP
; Use the codeIBROHIMXAIMLAPI
for 1 week FREE Access Let's get started! π±Deep Dive into AI/ML API Documentation (very detailed, canβt agree more): Click me, to get started π
Here's a brief tutorial: How to get API Key from AI/ML API. Quick step-by-step tutorial with screenshots for better understanding.
Judge0 from RapidAPI
Judge0 is a robust, scalable, and open-source online code execution system that can be used to build a wide range of applications that need online code execution features. Some examples include competitive programming platforms, e-learning platforms, candidate assessment and recruitment platforms, online code editors, online IDEs, and many more.
The full API documentation is available here.
Next.js
Next.js is a React framework that enables server-side rendering and static site generation for React applications. It provides a range of features that make it easier to build fast, scalable, and SEO-friendly web applications.
ps: I just love Next.js, it's my go-to framework for building React applications. π
Documentation: Next.js
Tailwind CSS
Tailwind CSS is a utility-first CSS framework that makes it easy to build custom designs without writing custom CSS. It provides a range of utility classes that can be used to style elements directly in the HTML.
Documentation: Tailwind CSS
Clerk Auth
Clerk is an authentication platform that provides a range of features for managing user authentication and authorization in web applications. It offers a range of features, including social login, multi-factor authentication, and user management.
Documentation: Clerk
Here's a brief tutorial on: How to create account on Clerk and setup new project
Vercel
Vercel is a cloud platform to deploy and host web applications. It offers a range of features, including serverless functions, automatic deployments, and custom domains.
Documentation: Vercel
Here's a brief tutorial: How to Deploy Apps to Vercel with ease
Prerequisites
Before we get started, make sure you have the following installed on your machine:
- Node.js. Here's a short tutorial on How to setup Node.js on my computer with ease.
- npm or yarn or yarn
- AI/ML API Key. Here's a tutorial on How to get API Key from AI/ML API
- Clerk Auth Account. Here's a brief tutorial on: How to create account on Clerk and setup new project
- Vercel Account. Here's a brief tutorial on: How to create account on Vercel
- Judge0 API Key; i will show you how to get it later. (upd: jump to Judge0 from RapidAPI)
Getting Started
Create a New Next.js Project
Let's get started by creating a new Next.js project:
npx create-next-app@latest
It will ask you a few *simple questions:
What is your project named? Here, you should enter your app name. For example: PrepAlly
(or whatever you wish). For the rest of the questions, simply hit enter:
Hereβs what youβll see:
β Would you like to use TypeScript? β¦ No / Yes
β Would you like to use ESLint? β¦ No / Yes
β Would you like to use Tailwind CSS? β¦ No / Yes
β Would you like your code inside a `src/` directory? β¦ No / Yes
β Would you like to use App Router? (recommended) β¦ No / Yes
β Would you like to use Turbopack for `next dev`? β¦ No / Yes
β Would you like to customize the import alias (`@/*` by default)? β¦ No / Yes
Open your project with Visual Studio Code:
cd PrepAlly
code .
API Routes
The first thing first, let's deal with API routes.
Create a new folder called api
in the root of your project. Inside the api
folder, create two new folders; query-gpt
and text-to-speech
.
Quick info:
query-gpt
will be used to query the GPT-4o model from the AI/ML API. It acts like a real interviewer, providing feedback, insights, and answers to questions.text-to-speech
will convert text to speech using the Deepgram Aura model from the AI/ML API, simulating the experience of interacting with a real human interviewer.
Enter the query-gpt
folder and create a new file called route.ts
. Put the following code in the file:
// app/api/query-gpt/route.ts
import { NextResponse } from 'next/server';
const apiKey = process.env.NEXT_PUBLIC_AIML_API_KEY;
export async function POST(request: Request) {
try {
console.log("POST /api/query-gpt");
const { messages } = await request.json();
console.log("input data: ", messages);
// Make the API call to the external service
const response = await fetch("https://api.aimlapi.com/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o",
messages: messages,
max_tokens: 512,
}),
});
if (!response.ok) {
// If the API response isn't successful, return an error response
return NextResponse.json({ error: "Failed to fetch completion data" }, { status: response.status });
}
const data = await response.json();
console.log("output data: ", data);
const assistantResponse = data.choices[0]?.message?.content || "No response available";
console.log("assistantResponse: ", assistantResponse);
// Return the assistant's message content
return NextResponse.json({ message: assistantResponse });
} catch (error) {
console.error("Error fetching the data:", error);
return NextResponse.json({ error: "An error occurred while processing your request." }, { status: 500 });
}
}
A$AP; Use the code
IBROHIMXAIMLAPI
for 1 week FREE Access Let's get started, bruh!
Next, enter the text-to-speech
folder and create a new file called route.ts
. Put the following code in the file:
// app/api/text-to-speech/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
console.log('POST /api/text-to-speech');
// Extract the text from the incoming request body
const { text } = await request.json();
console.log('user input:', text);
if (!text || text.length === 0) {
return NextResponse.json({ message: 'No text provided' }, { status: 400 });
}
const apiKey = process.env.NEXT_PUBLIC_AIML_API_KEY;
const apiResponse = await fetch('https://api.aimlapi.com/tts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`, // Replace with your actual API key
},
body: JSON.stringify({
model: '#g1_aura-asteria-en', // Replace with your specific model if needed
text: text
})
});
if (!apiResponse.ok) {
const errorData = await apiResponse.json();
return NextResponse.json(
{ message: errorData.message || 'Failed to fetch from ElevenLabs' },
{ status: apiResponse.status }
);
}
// Get the audio response as a blob
const audioBlob = await apiResponse.blob();
const arrayBuffer = await audioBlob.arrayBuffer();
// Return the binary audio file in the response
return new NextResponse(arrayBuffer, {
status: 200,
headers: {
'Content-Type': 'audio/mpeg',
'Content-Disposition': 'attachment; filename="audio.mp3"',
},
});
} catch (error: any) {
console.error('Error in /api/text-to-speech:', error);
return NextResponse.json(
{ error: error.message || 'Internal Server Error' },
{ status: 500 }
);
}
}
A$AP; Use the code
IBROHIMXAIMLAPI
for 1 week FREE Access Let's get started, bruh!
Don't hurry up! We should temporarily save the audio file
to play it later. So, the easiest way is thro using IndexedDB
. Let's create a new file called utils/indexdb.js
in the text-to-speech
folder:
export const openVoiceDatabase = async () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open('audioDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('audios', { keyPath: 'id' });
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
export const saveAndPlayAudio = async (blob) => {
const db = await openVoiceDatabase();
const audioId = 'audio_' + Date.now();
// Save to IndexedDB
await new Promise((resolve, reject) => {
const transaction = db.transaction(['audios'], 'readwrite');
const store = transaction.objectStore('audios');
const request = store.put({ id: audioId, audio: blob });
request.onsuccess = () => resolve();
request.onerror = (event) => reject(event.target.error);
});
// Create URL and play
const audioURL = URL.createObjectURL(blob);
const audio = new Audio(audioURL);
audio.play();
// Cleanup after playback
audio.addEventListener('ended', async () => {
URL.revokeObjectURL(audioURL);
const transaction = db.transaction(['audios'], 'readwrite');
const store = transaction.objectStore('audios');
store.delete(audioId);
console.log('Audio deleted from IndexedDB after playback.');
});
}
whoa! π€€
We have done with API routes. To sum up:
Above stuff (source code) demonstrates how to organize API routes in a project, enabling seamless interactions with external AI/ML APIs. Here's a brief explanation of how the pieces follow and complement each other:
-
API Folder Structure:
- The
api
folder serves as the root for organizing the application's API endpoints. Inside it, two subfolders (query-gpt
andtext-to-speech
) group related functionalities. Each subfolder corresponds to a specific feature (querying a model or converting text to speech).
- The
-
query-gpt
Route:- The
route.ts
file in thequery-gpt
folder defines the/api/query-gpt
endpoint. - This endpoint processes incoming POST requests with a
messages
payload, forwards them to the GPT-4o model using the AI/ML API, and returns the AI's response. - Key Highlights:
- Handles API authentication using a key from environment variables.
- Manages errors gracefully, returning appropriate status codes and messages.
- The
-
text-to-speech
Route:- The
route.ts
file in thetext-to-speech
folder defines the/api/text-to-speech
endpoint. - It accepts a
text
payload, forwards it to the AI/ML API to generate audio, and returns the audio file. - Key Highlights:
- Validates the input text and handles edge cases like empty inputs.
- Responds with audio as a binary file, including metadata like filename and content type.
- Incorporates error handling with detailed feedback.
- The
-
IndexedDB Utility for Audio:
- The
utils/indexdb.js
file provides functions for managing audio playback using IndexedDB. - It addresses the need to temporarily save and play audio files locally before cleaning up.
- Key Functions:
-
openVoiceDatabase()
: Opens or initializes an IndexedDB instance for storing audio files. -
saveAndPlayAudio(blob)
: Saves an audio blob to the database, plays it, and deletes it post-playback.
-
- The
-
Integration Flow:
- The
/api/query-gpt
endpoint acts as the "brains" of the interaction, providing intelligent responses. - The
/api/text-to-speech
endpoint transforms these responses into human-like audio. - The IndexedDB utility ensures the audio files are efficiently managed, enabling smooth playback without persisting unnecessary data.
- The
If you want more tutorials with IndexedDB
and text-to-speech
stuff. Kindly check this tutorial: Building a Chrome Extension from Scratch with AI/ML API, Deepgram Aura, and IndexedDB Integration
Now, let's move on to the next step.
Clerk Auth
Before we move on, let's set up the Clerk Auth for our application. Make sure you already set up a project on Clerk and have the API keys. If not, here's a brief tutorial on: How to create account on Clerk and setup new project
Install @clerk/nextjs
. The package to use with Clerk and NextJS.
npm install @clerk/nextjs
Set your environment variables. Add these keys to your .env.local
or create the file if it doesn't exist. Retrieve these keys anytime from the API keys page.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
Update middleware.ts
. Update your middleware file or create one at the root of your project or src/
directory if you're using a src/
directory structure. The clerkMiddleware
helper enables authentication and is where you'll configure your protected routes.
// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)'])
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect()
}
})
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}
Add ClerkProvider
to your app. All Clerk hooks and components must be children of the ClerkProvider
component. You can control which content signed in and signed out users can see with Clerk's prebuilt components.
Open app/layout.tsx
, add the following code:
// app/layout.tsx
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
// Import the ClerkProvider component
import {
ClerkProvider,
} from '@clerk/nextjs';
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const metadata: Metadata = {
title: "AI Coding Interview",
description: "AI Coding Interview is a platform that delivers real-time feedback and personalized insights, empowering candidates with the confidence and readiness to ace their interviews.",
};
// Wrap your app in the ClerkProvider component
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClerkProvider>
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{children}
</body>
</html>
</ClerkProvider>
);
}
Great! Now, we have set up the Clerk Auth for our application. But, we need to create a few more components to handle the authentication flow. For example: sign-in
and sign-up
components.
Let's enter app
and create a new two files exactly same as this:
sign-in/[[...sign-in]]/page.tsx
sign-up/[[...sign-up]]/page.tsx
ps: Otherwise it won't work.
Now update page.tsx
files with the following code corresponding to each file:
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'
export default function SignInPage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center p-24 relative text-white">
<div className="flex flex-col items-center justify-center h-full space-y-8">
<SignIn />
</div>
</div>
)
}
and,
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs'
export default function SignUpPage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center p-24 relative text-white">
<div className="flex flex-col items-center justify-center h-full space-y-8">
<SignUp />
</div>
</div>
);
}
PrepAlly Interface
Let's setup the necessary components for the PrepAlly interface. We'll create the following components:
- Code Editor Window
- Language Selector/Dropdown
- Coding Problems List
- Code Execution Button and Log
- FontAwesome Icons
- Assembling the PrepAlly Interface
Code Editor
We'll use the Monaco Editor for React
. It's a well-known web technology based code editor that powers VS Code
. Learn more.
Install the package:
npm install @monaco-editor/react
Create a new file called CodeEditorWindow.jsx
in the components
folder and add the following code:
// components/CodeEditorWindow.jsx
import React from "react";
import Editor from "@monaco-editor/react";
const CodeEditorWindow = ({ onChange, language, code }) => {
const handleEditorChange = (value) => {
onChange("code", value);
};
return (
<div className="overlay rounded-md overflow-hidden w-full h-full shadow-4xl w-[80%]">
<Editor
height="85vh"
width={`100%`}
language={language || "python"}
value={code}
defaultValue="# some comment"
onChange={handleEditorChange}
/>
</div>
);
};
export default CodeEditorWindow;
Language Selector
Next, let's create a language selector component. This component will allow users to select the programming language they want to use in the code editor.
Create a new file called LanguagesDropdown.jsx
in the components
folder and add the following code:
//components/LanguagesDropdown.jsx
import React from "react";
import Select from "react-select";
import { customStyles } from "../constants/customStyles";
import { languageOptions } from "../constants/languageOptions";
const LanguagesDropdown = ({ onSelectChange }) => {
return (
<Select
instanceId="language"
placeholder={`Filter By Category`}
options={languageOptions}
styles={customStyles}
defaultValue={languageOptions[2]}
onChange={(selectedOption) => onSelectChange(selectedOption)}
/>
);
};
export default LanguagesDropdown;
Now, let's create a constants
folder in the root of the project and add a new file called languageOptions.js
with the following code:
export const languageOptions = [
{
id: 63,
name: "JavaScript (Node.js 12.14.0)",
label: "JavaScript (Node.js 12.14.0)",
value: "javascript",
},
{
id: 43,
label: "Plain Text",
name: "Plain Text",
value: "text",
},
{
id: 71,
name: "Python (3.8.1)",
label: "Python (3.8.1)",
value: "python",
},
];
two languages are enough for now. π€ refer to
/app/constants/languageOptions.js
for other languages.
Coding Problems List
Next, let's create a component to display a list of coding problems. This component will allow users to select a problem to solve.
Create a new file called ProblemDropdown.tsx
in the components
folder and add the following code:
// components/ProblemDropdown.tsx
import React from "react";
import Select from "react-select";
import { customStyles } from "../../constants/customStyles";
import { problemsList } from "../../constants/problemsList";
const ProblemDropdown = ({ onSelectChange } : any) => {
return (
<Select
instanceId="problemDropdown"
placeholder={`Filter By Problem`}
options={problemsList}
styles={customStyles}
defaultValue={problemsList[0]}
onChange={(selectedOption) => onSelectChange(selectedOption)}
/>
);
};
export default ProblemDropdown;
Great! Now enter the constants
folder and create a new file called problemsList.ts
and add the following code:
// constants/problemsList.ts
export const problemsList = [
{
id: 1,
name: "Biggest Difference",
label: "Biggest Difference",
difficulty: "Easy",
value:
`
# Given an array length 1 or more of ints, return the difference between the largest and smallest values in the array.
# biggest_diff([10, 3, 5, 6]) => 7
# biggest_diff([7, 2, 10, 9]) => 8
# biggest_diff([2, 10, 7, 2]) => 8
def biggest_diff(nums):
`
},
{
id: 2,
name: "Biggest Difference",
label: "Cat & Doog",
difficulty: "Easy",
value:
`
# Return True if the string "cat" and "dog" appear the same number of times in the given string.
# cat_dog('catdog') => True
# cat_dog('catcat') => False
# cat_dog('1cat1cadodog') => True
def cat_dog(s):
`
},
{
id: 3,
name: "Biggest Difference",
label: "Sum 78",
difficulty: "Easy",
value:
`
# Write a function to return the sum of the numbers in the given array 'nums', except ignore sections of numbers starting with a 7 and extending to the next 8 (every 7 will be followed by at least one 8).
# Return 0 for no numbers.
# sum78([1, 2, 2]) => 5
# sum78([1, 2, 2, 7, 99, 99, 8]) => 5
# sum78([1, 1, 7, 8, 2]) => 4
def sum78(nums):
`
}
];
where did i get these problems? answer is here topmate.io/abdibrokhim
A little bit customization for the customStyles
. It's pretty similar as writing CSS in styles.css
file but in a more structured way.
// constants/customStyles.ts
export const customStyles = {
control: (styles) => ({
...styles,
width: "100%",
maxWidth: "14rem",
minWidth: "12rem",
borderRadius: "5px",
color: "#000",
fontSize: "0.8rem",
lineHeight: "1.75rem",
backgroundColor: "#FFFFFF",
cursor: "pointer",
border: "2px solid #000000",
boxShadow: "5px 5px 0px 0px rgba(0,0,0);",
":hover": {
border: "2px solid #000000",
boxShadow: "none",
},
}),
option: (styles) => {
return {
...styles,
color: "#000",
fontSize: "0.8rem",
lineHeight: "1.75rem",
width: "100%",
background: "#fff",
":hover": {
backgroundColor: "rgb(243 244 246)",
color: "#000",
cursor: "pointer",
},
};
},
menu: (styles) => {
return {
...styles,
backgroundColor: "#fff",
maxWidth: "14rem",
border: "2px solid #000000",
borderRadius: "5px",
boxShadow: "5px 5px 0px 0px rgba(0,0,0);",
};
},
placeholder: (defaultStyles) => {
return {
...defaultStyles,
color: "#000",
fontSize: "0.8rem",
lineHeight: "1.75rem",
};
},
};
Code Execution Button and Log
Next, let's create a component for the code execution button. This component will allow users to run their code and get real-time feedback.
Create a new file called RunButton.tsx
in the components
folder and add the following code:
// components/RunButton.tsx
import React from "react";
import { classnames } from "../utils/general";
import { faRocket } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const RunButton = ({ handleCompile, code, processing }) => {
return (
<button
onClick={handleCompile}
disabled={!code}
className={classnames(
"border-2 border-black z-10 rounded-md shadow-[5px_5px_0px_0px_rgba(0,0,0)] px-4 py-2 hover:shadow transition duration-200 bg-white flex-shrink-0",
!code ? "opacity-50" : ""
)}
>
{processing ? "Running... " : "Run "} <FontAwesomeIcon icon={faRocket} />
</button>
)
}
export default RunButton;
Here we can classnames
utility function to conditionally apply CSS classes based on the state of the button.
// utils/general.js
export const classnames = (...args) => {
return args.join(" ");
};
However, we can also achieve the same result w/o extra utility function. Simply:
className={`border-2 border-black z-10 rounded-md shadow-[5px_5px_0px_0px_rgba(0,0,0)] px-4 py-2 hover:shadow transition duration-200 bg-white flex-shrink-0 ${!code ? "opacity-50" : ""}`}
Next, let's create a component for the code execution log. This component will display the output of the code execution.
Create a new file called OutputWindow.jsx
in the components
folder and add the following code:
// components/OutputWindow.jsx
import React from "react";
import { faTerminal } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const OutputWindow = ({ outputDetails }) => {
const getOutput = () => {
let statusId = outputDetails?.status?.id;
if (statusId === 6) {
// compilation error
return (
<pre className="px-2 py-1 font-normal text-xs text-red-500">
{atob(outputDetails?.compile_output)}
</pre>
);
} else if (statusId === 3) {
return (
<pre className="px-2 py-1 font-normal text-xs text-green-500">
{atob(outputDetails.stdout) !== null
? `${atob(outputDetails.stdout)}`
: null}
</pre>
);
} else if (statusId === 5) {
return (
<pre className="px-2 py-1 font-normal text-xs text-red-500">
{`Time Limit Exceeded`}
</pre>
);
} else {
return (
<pre className="px-2 py-1 font-normal text-xs text-red-500">
{atob(outputDetails?.stderr)}
</pre>
);
}
};
return (
<>
<div className="flex items-center justify-between border-b">
<div className="font-normal px-4 py-2 text-md bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-700">
Execution Log {<FontAwesomeIcon icon={faTerminal} />}
</div>
{/* <button className="text-md">Close</button> */}
</div>
<div className="w-full bg-white px-2 mt-4 rounded-md text-black font-normal text-sm overflow-y-auto">
{outputDetails ? <>{getOutput()}</> : null}
</div>
</>
);
};
export default OutputWindow;
In addition, let's also add output details such as; status
, memory
, and time
.
Create>Enter OutputDetails.jsx
in the components
folder and add the following code:
// components/OutputDetails.jsx
import React from "react";
const OutputDetails = ({ outputDetails }) => {
return (
<div className="metrics-container px-4 mt-6 flex flex-col space-y-3">
<p className="text-xs">
Status:{" "}
<span className="font-semibold px-2 py-1 rounded-md bg-gray-100">
{outputDetails?.status?.description}
</span>
</p>
<p className="text-xs">
Memory:{" "}
<span className="font-semibold px-2 py-1 rounded-md bg-gray-100">
{outputDetails?.memory}
</span>
</p>
<p className="text-xs">
Time:{" "}
<span className="font-semibold px-2 py-1 rounded-md bg-gray-100">
{outputDetails?.time}
</span>
</p>
</div>
);
};
export default OutputDetails;
FontAwesome Icons
If you closely look at the code, we are using Font Awesome
icons. They are so nice. Learn more. Install it by running the following command:
npm i --save @fortawesome/fontawesome-svg-core
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/free-regular-svg-icons
npm i --save @fortawesome/free-brands-svg-icons
npm i --save @fortawesome/react-fontawesome@latest
Well, okey! Let's build the UI for the PrepAlly interface.
Assembling the PrepAlly Interface
Create a new file called PrepAlly.tsx
in the pages
folder. And quickly import the necessary components:
// pages/PrepAlly.tsx
import Image from "next/image";
import React, { useEffect, useState, useRef } from "react";
import CodeEditorWindow from "./CodeEditorWindow";
import axios from "axios";
import ReactMarkdown from 'react-markdown';
import { languageOptions } from "../constants/languageOptions";
import { problemsList } from "../constants/problemsList";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import defineTheme from "../lib/defineTheme";
import useKeyPress from "../hooks/useKeyPress";
import OutputWindow from "./OutputWindow";
import OutputDetails from "./OutputDetails";
import ThemeDropdown from "./ThemeDropdown";
import LanguagesDropdown from "./LanguagesDropdown";
import ProblemDropdown from "./problems/ProblemDropdown";
import RunButton from "./RunButton";
import { saveAndPlayAudio } from '../api/text-to-speech/utils/indexdb.js';
import { faClosedCaptioning, faMicrophone, faTerminal } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import loader from '../lib/loader';
import weiImage from '../assets/wei.jpeg';
import './styles.css';
import { useUser } from '@clerk/nextjs';
import { classnames } from "../utils/general";
Also don't forget to add 'use client'
to the top of the file. It's a new feature in Next.js that allows you to use the client-side API in your server-side code.
Notifications
Notifications are important. Let's create a few functions to show success, error, and info messages using react-toastify
.
Install the package:
npm install react-toastify
Create separate functions for each type of notification:
const showSuccessToast = (msg:string) => {
toast.success(msg || `Compiled Successfully!`, {
position: "top-right",
autoClose: 1000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
};
const showErrorToast = (msg:string, timer:any) => {
toast.error(msg || `Something went wrong! Please try again.`, {
position: "top-right",
autoClose: timer ? timer : 1000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
};
const showInfoToast = (msg:string) => {
toast.info(msg || `Processing your request...`, {
position: "top-right",
autoClose: 1000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
};
And include in return
statement:
<>
<ToastContainer
position="top-right"
autoClose={2000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
// ...rest of the code
</>
Here's how the notification card looks like. It's on the top-right corner of the screen:
Code Compilation
To compile the code, we'll use the Judge0 as I mentioned earlier. We'll make a POST request to the Judge0 API to compile the code and get the output. But, we'll need the API URL
, host
, and key
.
Lemme show you how to get these keys:
Go to RapidAPI.
Create an account or login if you already have one.
- Search for
Judge0
and subscribe to the API.
- Get the
API URL
,host
, andkey
.
Quick info: on the left side you can see the endpoints
and on the top parameters/payloads/header/auth
etc. On the right side there is a code snippet
that you can use to make a request. Before copying the code snippet, make sure to select the language (e.g., Python, Javascript, and etc.) you want to use and how (e.g., requests, axios, and etc.).
ps; in our case it's
axios
andJavascript
.
Initialize the state variables for the code editor:
const [code, setCode] = useState(problemsList[0].value);
const [outputDetails, setOutputDetails] = useState(null);
const [processing, setProcessing] = useState(false);
const [language, setLanguage] = useState(languageOptions[2]);
Add a function to handle the code execution. This function will compile the code using the RapidAPI and display the output in the output window. It also checks the API call limit and displays an error message if the limit is reached.
// we will show this alert if the user selects the language that is not supported by the API
const handleAgent = () => {
alert("The agent is not available at the moment");
};
const handleCompile = () => {
// Check if the API call limit has been reached
const apiCallLimit = 2;
const apiCallLimitDuration = 5 * 60 * 1000; // 5 minutes in milliseconds
const currentTimestamp = new Date().getTime();
if (localStorage.getItem("apiCallCount")) {
const apiCallCount = parseInt(localStorage.getItem("apiCallCount")!);
const firstApiCallTime = parseInt(localStorage.getItem("firstApiCallTime")!);
if (apiCallCount >= apiCallLimit && currentTimestamp - firstApiCallTime < apiCallLimitDuration) {
// API call limit reached, show an error message
showErrorToast("API call limit reached. Please wait for 5 minutes before making more API calls.", 1000);
return;
}
} else {
// Set the initial values in local storage
localStorage.setItem("apiCallCount", "0");
localStorage.setItem("firstApiCallTime", currentTimestamp.toString());
}
// Increment the API call count in local storage
const apiCallCount = parseInt(localStorage.getItem("apiCallCount")!);
localStorage.setItem("apiCallCount", (apiCallCount + 1).toString());
// Proceed with the API call
setProcessing(true);
const formData = {
language_id: language.id,
// encode source code in base64
source_code: btoa(code),
stdin: btoa(customInput),
};
const options = {
method: "POST",
url: process.env.NEXT_PUBLIC_RAPID_API_URL,
params: { base64_encoded: "true", wait: 'false', fields: "*" },
headers: {
"content-type": "application/json",
"Content-Type": "application/json",
"X-RapidAPI-Host": process.env.NEXT_PUBLIC_RAPID_API_HOST,
"X-RapidAPI-Key": process.env.NEXT_PUBLIC_RAPID_API_KEY,
},
data: formData,
};
axios
.request(options)
.then(function (response) {
console.log("res.data", response.data);
const token = response.data.token;
checkStatus(token);
})
.catch((err) => {
let error = err.response ? err.response.data : err;
// get error status
let status = err.response.status;
console.log("status", status);
if (status === 429) {
console.log("too many requests", status);
showErrorToast(
`Quota of 50 requests exceeded for the Day!`,
10000
);
}
setProcessing(false);
console.log("catch block...", error);
});
};
Check the status of the code compilation. If the code is still processing, the function will check the status again after a delay. If the code compilation is successful, the output details will be displayed in the output window.
const checkStatus = async (token:string) => {
const options = {
method: "GET",
url: process.env.NEXT_PUBLIC_RAPID_API_URL + "/" + token,
params: { base64_encoded: "true", fields: "*" },
headers: {
"X-RapidAPI-Host": process.env.NEXT_PUBLIC_RAPID_API_HOST,
"X-RapidAPI-Key": process.env.NEXT_PUBLIC_RAPID_API_KEY,
},
};
try {
let response = await axios.request(options);
let statusId = response.data.status?.id;
// Processed - we have a result
if (statusId === 1 || statusId === 2) {
// still processing
setTimeout(() => {
checkStatus(token);
}, 2000);
return;
} else {
setProcessing(false);
setOutputDetails(response.data);
showSuccessToast(`Compiled Successfully!`);
console.log("response.data", response.data);
return;
}
} catch (err) {
console.log("err", err);
setProcessing(false);
showErrorToast(`Something went wrong! Please try again.`, 1000);
}
};
Update return
statement to include the components; CodeEditorWindow
, LanguagesDropdown
, ProblemDropdown
, RunButton
, and OutputWindow
.
// ...rest of the code
<div className="flex flex-col sm:flex-row">
<div className="px-4 py-2">
<ProblemDropdown onSelectChange={onProblemChange} />
</div>
<div className="px-4 py-2">
<LanguagesDropdown onSelectChange={onLanguageChange} />
</div>
<div className="px-4 py-2">
<RunButton handleCompile={language.id !== 43 ? handleCompile : handleAgent} code={code} processing={processing}/>
</div>
<div className="px-4 py-2">
<button className="border-2 border-black z-10 rounded-md shadow-[5px_5px_0px_0px_rgba(0,0,0)] px-4 py-2 hover:shadow transition duration-200 bg-white flex-shrink-0" onClick={toggleExecutionLog}>
Execution log {<FontAwesomeIcon icon={faTerminal} />}
</button>
</div>
</div>
Problem dropdown feature:
Language dropdown feature:
Add the CodeEditorWindow
component to the return
statement:
// ...rest of the code
<div className="flex flex-col h-full">
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4 items-start px-4 py-4">
<div className="flex flex-row w-full h-full justify-start items-end">
<CodeEditorWindow
code={code}
onChange={onChange}
language={language?.value}
/>
{/* interviewer window */}
<div className="flex flex-col items-center justify-center text-center w-[20%] mb-[50px]">
<div className="flex flex-col text-center items-center justify-center gap-2 noselect">
{/* Circular GIF background with image on top */}
<div className="relative w-32 h-32 rounded-full overflow-hidden shadow-lg">
{/* GIF background */}
<div className="absolute inset-0 w-[142%] h-[142%] mt-[-26px] ml-[-25px] bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: `url(/circle.gif)` }}>
</div>
{/* Image layered on top */}
<Image
priority={true}
src={weiImage}
width={80}
height={80}
alt="Interviewer"
className="relative w-24 h-24 rounded-full shadow-md nodrag top-4 left-4"
title="Interviewer"
/>
</div>
<p className="text-lg font-bold">{interviewerName}</p>
<p>{getInterviewState()}</p>
</div>
</div>
</div>
</div>
</div>
// ...rest of the code
Code editor window:
AI Coding Interviewer:
Implement rest of the functions:
const [selectedProblem, setSelectedProblem] = useState(problemsList[0]);
const onLanguageChange = (sl:any) => {
console.log("selected Option...", sl);
setLanguage(sl);
};
const onProblemChange = async (selectedProblem:any) => {
console.log("selected Option...", selectedProblem);
setSelectedProblem(selectedProblem);
setCode(selectedProblem.value);
setInterviewerState({
isThinking: true,
isSpeaking: false,
isListening: false,
});
await prepareInitialPromptForSpeech();
};
const interviewerName = "Wei B Tan";
const enterPress = useKeyPress("Enter");
const ctrlPress = useKeyPress("Control");
useEffect(() => {
if (enterPress && ctrlPress) {
console.log("enterPress", enterPress);
console.log("ctrlPress", ctrlPress);
handleCompile();
}
}, [ctrlPress, enterPress]);
const onChange = (action:any, data:any) => {
switch (action) {
case "code": {
setCode(data);
break;
}
default: {
console.warn("case not handled!", action, data);
}
}
};
const handleAgent = () => {
alert("The agent is not available at the moment");
};
There's also circled gif; url(/circle.gif)
around the interviewer image. It gives a nice effect; kinds speaking etc.
Define the getInterviewState
function to display the current state of the interviewer:
const [interviewerState, setInterviewerState] = useState({
isThinking: false,
isSpeaking: false,
isListening: false,
});
// check interview state and return string
const getInterviewState = () => {
if (interviewerState.isThinking) {
return 'Thinking...';
} else if (interviewerState.isSpeaking) {
return 'Speaking...';
} else if (interviewerState.isListening) {
return 'Listening...';
} else {
return 'Idle...';
}
};
Execution Log
Make execution log window resizable:
const [executionLogHeight, setExecutionLogHeight] = useState(200);
const [resizing, setResizing] = useState(false);
useEffect(() => {
if (resizing) {
const handleMouseMove = (event: any) => {
const newHeight = window.innerHeight - event.clientY;
const clampedHeight = Math.max(100, Math.min(newHeight, 500));
setExecutionLogHeight(clampedHeight);
};
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}
}, [resizing]);
const toggleExecutionLog = () => {
setShowExecutionLog(!showExecutionLog);
};
const handleMouseDown = (event:any) => {
setResizing(true);
};
const handleMouseUp = () => {
setResizing(false);
};
Now, add execution log and output details to the return
statement:
// ...rest of the code
<div className="relative">
{showExecutionLog && (
<>
<div
className={`fixed left-0 right-0 bottom-0 bg-white border-t border-gray-300 overflow-y-auto z-50 ${
resizing ? "pointer-events-none" : ""
}`}
style={{ height: `${executionLogHeight + 1}px`, cursor: "row-resize", }}
onMouseDown={handleMouseDown}
></div>
<div
className="fixed left-0 right-0 bottom-0 bg-white border-gray-300 overflow-y-auto z-50"
style={{ height: `${executionLogHeight}px`, maxHeight: "500px", minHeight: "100px", }}
>
<div className="">
<OutputWindow outputDetails={outputDetails} />
{outputDetails && <OutputDetails outputDetails={outputDetails} />}
</div>
</div>
</>
)}
</div>
// ...rest of the code
Execution log:
Okey, let's work on RecordButton
.
// ...rest of the code
<div className="px-4 py-2">
<button
onClick={()=>{handleRecordButton()}}
className={classnames("border-2 border-black z-10 rounded-md shadow-[5px_5px_0px_0px_rgba(0,0,0)] px-4 py-2 hover:shadow transition duration-200 bg-white flex-shrink-0",)}
>
{isRecording ? "Stop " : "Record " } {isRecording ? loader() : <FontAwesomeIcon icon={faMicrophone} />}
</button>
</div>
<div className="px-4 py-2">
<button
onClick={()=>{setIsShowingChatLogs(!isShowingChatLogs)}}
className={classnames("border-2 border-black z-10 rounded-md shadow-[5px_5px_0px_0px_rgba(0,0,0)] px-4 py-2 hover:shadow transition duration-200 bg-white flex-shrink-0",)}
>
{isShowingChatLogs ? "Hide chat " : "Show chat "} <FontAwesomeIcon icon={faClosedCaptioning} />
</button>
</div>
// ...rest of the code
Here's real demo, how it works: PrepAlly
Chat logs will be displayed in a fixed window on the right side of the screen. The window will contain a list of chat messages. It's kinda transcript of the whole conversation. Super useful to back to the conversation and see what was discussed if you missed something.
const [isShowingChatLogs, setIsShowingChatLogs] = useState(false);
{isShowingChatLogs && (
<div className="fixed top-16 right-10 w-[400px] h-[400px] bg-white border border-gray-300 rounded-lg shadow-lg overflow-hidden z-50">
<div className="p-4 bg-gray-800 text-white text-center font-bold">Chat</div>
<div className="p-4 h-[calc(100%-60px)] overflow-y-auto space-y-3 bg-gray-100">
{chatLogs.map((log, index) => (
<div
key={index}
className={`p-3 rounded-lg text-sm ${
log.role === "user" ? "bg-gray-200 text-right" : "bg-gray-300 text-left"
}`}
>
<ReactMarkdown
components={{
a: ({ node, ...props }) => (
<a className="text-blue-800 cursor-pointer" {...props} />
),
}}
>
{log.content}
</ReactMarkdown>
</div>
))}
</div>
</div>
)}
Here since AI answers comes in markdown format, we use ReactMarkdown
to render the markdown content.
Chat logs:
Then, bunch of functions
, functions, functions...
// State variables for speech recognition
const [isRecording, setIsRecording] = useState(false);
const [recordingComplete, setRecordingComplete] = useState(false);
const [ctranscript, setcTranscript] = useState('');
// Reference to store the SpeechRecognition instance
const recognitionRef = useRef<any>(null);
// Start Recording
const startRecording = async () => {
console.log('Starting recording...');
setIsRecording(true);
setRecordingComplete(false);
setcTranscript('');
// update state
setInterviewerState({
isThinking: false,
isSpeaking: false,
isListening: true,
});
recognitionRef.current = new window.webkitSpeechRecognition();
recognitionRef.current.continuous = true;
recognitionRef.current.interimResults = false;
recognitionRef.current.lang = 'en-US';
// Updated onresult handler
recognitionRef.current.onresult = (event:any) => {
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
}
}
console.log('Final transcript: ', finalTranscript);
if (finalTranscript.length > 0) {
setcTranscript(finalTranscript);
addChatLogs({ role: 'user', content: finalTranscript });
const msg = `[Code]\n${code}\n\n [User Query & Response]\n${finalTranscript}`;
addMessageLogs({ role: 'user', content: msg });
handleAIResponse(msg);
} else {
alert('No speech detected. Please try again.');
}
};
recognitionRef.current.onerror = (event:any) => {
console.error('Speech recognition error', event.error);
alert('Speech recognition error: ' + event.error);
setIsRecording(false);
};
recognitionRef.current.onend = () => {
console.log('Speech recognition ended');
setIsRecording(false);
setInterviewerState({
isThinking: true,
isSpeaking: false,
isListening: false,
});
};
recognitionRef.current.onspeechend = () => {
recognitionRef.current.stop();
recognitionRef.current.continuous = false;
};
recognitionRef.current.start();
};
// Stop Recording
const stopRecording = async () => {
if (recognitionRef.current) {
console.log("Stopping recording")
setIsRecording(false);
setInterviewerState({
isThinking: true,
isSpeaking: false,
isListening: false,
});
recognitionRef.current.stop();
}
};
// Toggle Recording
const handleRecordButton = () => {
console.log("handleRecordButton...");
if (!isRecording) {
startRecording();
} else {
stopRecording();
}
};
// Cleanup effect
useEffect(() => {
return () => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
};
}, []);
I was really brief on the speech recognition part. If you want to learn more about it. Kindly check this tutorial: Building a Chrome Extension from Scratch with AI/ML API, Deepgram Aura, and IndexedDB Integration
The important stuff: After 'use client'
declare a global interface to add the webkitSpeechRecognition property to the Window object. This is necessary to avoid TypeScript errors when using the webkitSpeechRecognition
API. It will be used to enable voice input in the code editor.
declare global {
interface Window {
webkitSpeechRecognition: any;
}
}
Tha main part of the code is the handleAIResponse
function. This function will handle the response from the AI model. It will prepare the chat messages, send the user query to the GPT-4o model, and convert the AI reply to speech. It's kinds wrapper function for the AI model.
// ================================================
// cookin ai stuff...
// Update handleAIResponse function
const handleAIResponse = async (userQuery:string) => {
console.log('Handling AI response...');
showInfoToast('Processing...');
try {
// Show some loading state if needed
console.log('Current user query:', userQuery);
console.log('Current chat logs: ', chatLogs);
console.log('Current message logs: ', messagesLogs);
const chatMessages = prepareChatMessages(userQuery);
console.log('Prepared chat messages:', chatMessages);
// Send the transcribed text to the GPT-4o model
const aiReply = await generateReply(chatMessages);
console.log('AI Reply:', aiReply);
// Update chat logs
addChatLogs({ role: 'assistant', content: aiReply });
// Update messages logs
addMessageLogs({ role: 'assistant', content: aiReply });
// Convert the AI reply to speech and play it
await textToSpeech(aiReply);
console.log("I should be printed after textToSpeech, um..., shitt.");
} catch (error) {
console.error('Error handling AI response:', error);
showErrorToast('An error occurred while processing your request.', 2000);
} finally {
}
};
The generateReply
function will send the user query to the GPT-4o model and return the AI reply.
// send request to gpt-4o
// generate reply for user query
const generateReply = async (messages:any) => {
console.log('Generating reply...');
try {
// query-model
const response = await fetch('/api/query-gpt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ messages }),
});
if (!response.ok) {
throw new Error('API request failed');
}
const data = await response.json();
return data.message;
} catch (error) {
console.error('Error:', error);
alert('An error occurred while fetching the reply.');
return 'No response available';
}
};
The textToSpeech
function will convert the AI reply to speech and play it.
// when we get reply from gpt-4o model then we will convert it to voice and play it
// send request to elevenlabs api
// text to speech
const textToSpeech = async (text: string) => {
console.log('Converting text to speech...');
try {
const response = await fetch('/api/text-to-speech', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text }),
});
if (!response.ok) {
throw new Error('API request failed');
}
const blob = await response.blob();
// Save to IndexedDB and play
setInterviewerState({
isThinking: false,
isSpeaking: true,
isListening: false,
});
await saveAndPlayAudio(blob);
} catch (error) {
console.error('Error:', error);
alert('An error occurred while fetching the audio.');
} finally {
// startRecording();
}
};
Above if you were pretty much following along, you should see prepareChatMessages
function. It's a helper function to prepare chat messages for the GPT-4o model. It will format the chat messages in a way that we can send them to the model for processing.
This function is really important for AI Coding Interviewer.
So, it will have a lot of information about the user, the problem, and the conversation between the user and the AI assistant. Otherwise, the AI model or GPT-4o
will not be able to provide a meaningful response. Simply, it will be lost in the conversation.
const prepareChatMessages = (userMessage:string) => {
const currentUser = user?.fullName || 'Dear';
const currentProblem = selectedProblem?.label || 'problem';
const currentProblemContent = selectedProblem?.value || 'problem content';
const tempInstr = `
${systemPrompt}\n
You are talking to ${currentUser}.\n
Problem: ${currentProblem}\n
Here is Problem Statement: ${currentProblemContent}\n
Below given Conversation between you and ${currentUser}.\n
If user asked any question please, answer the question.\n
Provide feedback to their code.\n
`;
const newMessageLog = { role: 'user', content: userMessage };
const updatedMessagesLogs = [...messagesLogs, newMessageLog];
const messages = [
{
role: "system",
content: tempInstr
},
...updatedMessagesLogs,
];
return messages;
};
On user?.fullName
we are getting it from the Clerk
user object.
Next, prepare very initial prompt for speech. When user enters the page, the AI assistant will greet the user and provide some information about the problem.
const prepareInitialPromptForSpeech = async () => {
const currentUser = user?.firstName || 'Dear';
const currentProblem = selectedProblem?.label || 'problem';
const currentProblemContent = selectedProblem?.value || 'problem content';
const tempInstr = `
${systemPrompt}
\nYou will be given a [New Problem] that you should paraphrase and return. Your paraphrased problem statement should be concise and informative. It should be a clear and accurate representation of the original problem statement. If you need example paraphrases, you can refer to the examples provided below. Below you can find the [example actual Problem Statement] and [example Paraphrased Problem Statement].\n
[example actual Problem Statement]\n${currentProblemContent}\n\n[example Paraphrased Problem Statement]\nWrite a function to calculate the sum of numbers in an array while ignoring sections starting with a 7 and ending with the next 8.
`;
const messages = [
{
role: "system",
content: tempInstr
},
{
role: "user",
content: currentProblemContent
},
];
const paraphrasedProblemStatement = await generateReply(messages);
console.log('Paraphrased Problem Statement:', paraphrasedProblemStatement);
const initialPromptSpeech = `Welcome, ${currentUser}! I'm ${interviewerName}, and I'm currently a Senior Software Engineer at Snapchat. Today, we'll be working on the ${currentProblem} problem, where ${paraphrasedProblemStatement}. Please take a minute to read the problem and respond when you're ready to work on it.`;
console.log('Initial Prompt Speech:', initialPromptSpeech);
// update chat logs
addChatLogs({ role: 'assistant', content: initialPromptSpeech });
// udpate messages logs
addMessageLogs({ role: 'assistant', content: initialPromptSpeech });
// Convert the initial prompt to speech and play it
await textToSpeech(initialPromptSpeech);
};
Function to add new log and trigger update
// Function to add new log and trigger update
const addChatLogs = (newMessage:any) => {
setChatLogs((prevLogs) => [...prevLogs, newMessage]);
};
const [userInteracted, setUserInteracted] = useState(false);
useEffect(() => {
const handleUserInteraction = () => {
setUserInteracted(true);
window.removeEventListener('click', handleUserInteraction);
};
window.addEventListener('click', handleUserInteraction);
return () => {
window.removeEventListener('click', handleUserInteraction);
};
}, []);
useEffect(() => {
if (userInteracted) {
setInterviewerState({
isThinking: true,
isSpeaking: false,
isListening: false,
});
prepareInitialPromptForSpeech();
}
}, [userInteracted]);
When user enters the page for the first time.
Let's add very simple yet nice loader()
:
const loader = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24">
<circle cx={4} cy={12} r={3} fill="currentColor">
<animate id="svgSpinners3DotsScale0" attributeName="r" begin="0;svgSpinners3DotsScale1.end-0.25s" dur="0.75s" values="3;.2;3" />
</circle>
<circle cx={12} cy={12} r={3} fill="currentColor">
<animate attributeName="r" begin="svgSpinners3DotsScale0.end-0.6s" dur="0.75s" values="3;.2;3" />
</circle>
<circle cx={20} cy={12} r={3} fill="currentColor">
<animate id="svgSpinners3DotsScale1" attributeName="r" begin="svgSpinners3DotsScale0.end-0.45s" dur="0.75s" values="3;.2;3" />
</circle>
</svg>
);
In addition, let's add welcome message when the user first time visit the page:
useEffect(() => {
defineTheme("active4d").then((_) =>
setTheme({ value: "active4d", label: "Active4D" })
);
showSuccessToast("Welcome to Code Editor!");
}, []);
Example:
Next step let's quickly set up environment variables and test it locally.
Environment Variables
Open .env
file and add the following environment variables:
NEXT_PUBLIC_RAPID_API_URL=https://judge0-ce.p.rapidapi.com/submissions
NEXT_PUBLIC_RAPID_API_HOST=judge0-ce.p.rapidapi.com
NEXT_PUBLIC_RAPID_API_KEY=...
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_AIML_API_KEY=...
Run Locally
Now, you can run the application locally with the following command:
npm run dev
Open http://localhost:3000 in your browser to see the application running.
You should see something similar to this:
Here on the bottom left side you can see your profile account:
Feel free to solve the first problem and run the code:
Here's how real interview looks like:
Watch the interview in action:
If you want to learn more about Building AI powered projects or whatever. Let me know. It's FREE! π -> Learn now!
So, that's it! But, we are not done yet. We need to deploy our application to Vercel.
Deploy to Vercel
To deploy the application to Vercel, you need to create a Vercel account. Please follow this tutorial to deploy your Next.js application to Vercel: How to Deploy Apps to Vercel withΒ ease.
Once you have deployed the application, you can try it out and share it with your peers.
ps; as i did here with my close friend:
pss; you can also watch the Uncensored π version on Patreon here [Uncensored]: PrepAlly, an Open Source and AI-powered Interview Preparation Platform.
Hype it up!
Let's gooo! π¦
ProductHunt
Ok, firstly we should create an account on ProductHunt here Create an account.
Then, we can submit our project there.
Click on the Submit
button on the top right corner. Paste the URL of the project and click on Submit
. Next it will ask you to fill the details about the project. Take your time and fill the details.
More details: π
Then, click Schedule for Later
button and select the date and time you want to launch the project.
Finally, click on Schedule
button. That's it! π
Here's the link to the project: PrepAlly on ProductHunt. How about yours? Let me know in the comments below or message me topmate.io/abdibrokhim. Really, I would love to see your project. π
X (formerly Twitter)
The very effective way to promote your project is to share it on X. Just drop some postshit and voila! π¦
For example, look at this description:
Introducing PrepAlly, an Open Source and AI-powered Interview Preparation Platform.
- Select the problem from the list.
- Choose your programming language.
- Write the code and run it instantly.
- Talk to AI and get feedback on your code.
- Feel like you are in a real interview.
Also, upload the video of the demo directly. (ps; don't put link to YouTube, instead upload the video directly)
Here's the link to the post: PrepAlly on X. How about yours? Let me know in the comments below or message me topmate.io/abdibrokhim.
Conclusion
In this tutorial, we built an AI-powered coding interview platform using Next.js, React, Tailwind CSS, and AI/ML API. We integrated the platform with Clerk Auth for authentication and deployed it to Vercel. We also added features like voice input, chat logs, and execution logs to enhance the user experience.
We also learned how to promote the project on ProductHunt and X to reach a wider audience. At least, we hyped it up! π
ps; now you are pretty much ready to apply YC. Here's the link on how to apply to YC: How to Apply to Y Combinator
I hope you enjoyed building this project and learned something new. If you have any questions or feedback, feel free to message me topmate.io/abdibrokhim. I would love to hear from you. π«
All the code for this project is available on GitHub: PrepAlly; AI Coding Interviewer. Open Source π.
Save this tutorial for later reference: Let's build Startup. Step-by-Step Tutorial on Building AI Coding Interviewer (e.g., PrepAlly) with AI/ML API and Integration with Clerk Auth and Deploying to Vercel. (it's always available on Medium) and Dev Community for FREE! π
Other interesting tutorials:
with step-by-step guide and screenshots:
on Medium:
on Dev Community:
Try what you have built so far (if you followed along π):
- HumanAIze AI text tool, it's absolutely FREE! π and The smartest AI humanizer.
- AI Sticker Maker Platform on HuggingFace Spaces π. You can print them and make your life funnier.
- Chrome Extension to Read Aloud the text on the webpage.
- Not a subscriber? Download Loom Videos for free using lovido.lol. Open Source, put a star on it! βοΈ
GPTs (i did during the hackathons):
- StoryAI, Where Climate Data Meets Conversation π
- EcoShopAI, I help you to make eco-friendly purchasing decisions with minimal environmental impact
- AI Sticker Maker, I will create really cutesy stickers for you π
Get Started with AI/ML API for FREE ($0 US dollars): Click me, let's Cook, bro! π§βπ³
A$AP; Use the code
IBROHIMXAIMLAPI
for 1 week FREE Access Let's get started, bruh! π±
Tutorial was cooked by Ibrohim Abdivokhidov, (follow this π on LinkedIn). Why, umm... why not tho?
ps: [Uncensored]: Founders video; Y Combinator Winter 2025 batch be a Patron
you need someone to guide you through the challenges? iβm here to help Book a Call
pss: 1οΈβ£ AI/ML API Regional Ambassador in Central Asia | founder CEO at Open Community (150+ π§βπ») | Hacker (60+ hackathons π¦) | Open Source contributor at Anarchy Labs (477+ βοΈ), Langflow (31,2K+ βοΈ) | Mentor (200K+ π§βπ) | Author (5+ π)... umm and more stuff cookin' up -> imcook.in !
Top comments (0)