Skip to content

Commit

Permalink
upload project
Browse files Browse the repository at this point in the history
  • Loading branch information
zaurbek-stark authored Mar 24, 2024
0 parents commit f9f009a
Show file tree
Hide file tree
Showing 26 changed files with 7,661 additions and 0 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Resume Worth Calculator

> This app was built as part of a challenge in the **Codebender AI Bootcamp**. Learn how to build projects like these [here](https://lastcodebender.com/bootcamp).
This app estimates the dollar worth of your resume (i.e. how much money you can make with your resume).

This project is built using Next.js and the Mistral API. The content from the resume pdf is extracted by PDF.js.

<img src="resumeworth-demo.gif" alt="app demo" width=600>

## Author

This project is built by Zaurbek Stark (The Codebender).

- [YouTube](https://www.youtube.com/@thecodebendermaster)
- [𝕏/Twitter](https://twitter.com/ZaurbekStark)

## Getting Started

First, duplicate the `.env` file into a new file named `.env.local`. Update the value of your Mistral API key there. To get a key, you need to sign up on https://console.mistral.ai/

The first time you are running this project, you will need to install the dependencies. Run this command in your terminal:

```bash
yarn
```

To start the app, run:

```bash
yarn dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
53 changes: 53 additions & 0 deletions app/api/openai-gpt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import OpenAI from 'openai';
import { OpenAIStream, StreamingTextResponse } from 'ai';

export const runtime = 'edge';

export async function POST(req: Request, res: Response) {
const { apiKey, messages } = await req.json();

const openai = new OpenAI({
apiKey
});

const response = await openai.chat.completions.create({
model: "gpt-4-1106-preview",
messages: [
{
role: "system",
content: `CONTEXT: You are an expert at predicting the dollar worth of resumes.
-------
TASK:
- You will receive a resume from a user as a test input.
- Analyze the resume and provide an estimated worth in US dollars
- Provide 4 short bullet points explanation of the key factors contributing to the assessment,
and 4 tips on how they can improve their worth. Each bullet point should be less than 1 line.
-------
OUTPUT FORMAT:
<Estimated Worth>$...</Estimated Worth>
<Explanation>
<ul>
<li>...</li>
<li>...</li>
<li>...</li>
...
</ul>
</Explanation>
<Improvements>
<ul>
<li>...</li>
<li>...</li>
<li>...</li>
...
</ul>
</Improvements>`
},
...messages,
],
stream: true,
temperature: 1,
});

const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
}
49 changes: 49 additions & 0 deletions app/api/resume/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import MistralClient from '@mistralai/mistralai';
import { MistralStream, StreamingTextResponse } from 'ai';

const mistral = new MistralClient(process.env.MISTRAL_API_KEY || '');

export const runtime = 'edge';

export async function POST(req: Request) {
const { prompt } = await req.json();

const response = mistral.chatStream({
model: 'mistral-large-latest',
messages: [{
role: 'user',
content: `CONTEXT: You are an expert at predicting the dollar worth of resumes.
-------
TASK:
- Analyze the resume given below and provide its estimated worth in US dollars
- Provide 4 short bullet points explanation of the key factors contributing to the assessment,
and 4 tips on how they can improve their worth. Each bullet point should be less than 1 line.
-------
RESUME:
${prompt}
-------
OUTPUT FORMAT:
<Estimated Worth>$...</Estimated Worth>
<Explanation>
<ul>
<li>...</li>
<li>...</li>
<li>...</li>
...
</ul>
</Explanation>
<Improvements>
<ul>
<li>...</li>
<li>...</li>
<li>...</li>
...
</ul>
</Improvements>`
}],
});

const stream = MistralStream(response);

return new StreamingTextResponse(stream);
}
49 changes: 49 additions & 0 deletions app/components/ResumeAnalyzerApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useEffect, useState } from 'react';
import ResumeUploader from './ResumeUploader';
import ResumeWorth from './ResumeWorth';
import styles from '../styles/ResumeAnalyzerApp.module.css';
import { useCompletion } from 'ai/react';

const ResumeAnalyzerApp = () => {
const [showWorth, setShowWorth] = useState(false);
const [isLoadingResume, setIsLoadingResume] = useState(false);
const [resumeText, setResumeText] = useState<string>('');
const {
completion,
isLoading,
complete,
error,
} = useCompletion({
api: '/api/resume',
});
console.log('completion:', completion);

useEffect(() => {
const getResumeWorth = async (text: string) => {
const messageToSend = `RESUME: ${text}\n\n-------\n\n`;
await complete(messageToSend);
setShowWorth(true);
};

if (resumeText !== '') {
getResumeWorth(resumeText).then();
}
}, [resumeText]);

return (
<div>
{!showWorth ? (
<div className={styles.uploaderWrapper}>
<p className={styles.instructionsText}>Upload your resume to know your worth.</p>
<ResumeUploader setIsLoading={setIsLoadingResume} setResumeText={setResumeText} />
{(isLoadingResume || isLoading) && <div className={styles.loadingSpinner}></div>}
</div>
) : (
<ResumeWorth resumeWorth={completion} />
)}
{error && <p className={styles.errorMessage}>{error.message}</p>}
</div>
);
};

export default ResumeAnalyzerApp;
91 changes: 91 additions & 0 deletions app/components/ResumeUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useState } from 'react';
import type { TextContent, TextItem } from 'pdfjs-dist/types/src/display/api';
import styles from '../styles/ResumeUploader.module.css';
import { MdCloudUpload } from "react-icons/md";

type Props = {
setResumeText: React.Dispatch<React.SetStateAction<string>>;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
};

const ResumeUploader: React.FC<Props> = ({ setResumeText, setIsLoading }) => {
const [error, setError] = useState('');

const mergeTextContent = (textContent: TextContent) => {
return textContent.items
.map((item) => {
const { str, hasEOL } = item as TextItem;
return str + (hasEOL ? '\n' : '');
})
.join('');
};

const readResume = async (pdfFile: File | undefined) => {
const pdfjs = await import('pdfjs-dist');
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;

if (!pdfFile) return;

const reader = new FileReader();
reader.onload = async (event) => {
const arrayBuffer = event.target?.result;
if (arrayBuffer && arrayBuffer instanceof ArrayBuffer) {
const loadingTask = pdfjs.getDocument(new Uint8Array(arrayBuffer));
loadingTask.promise.then(
(pdfDoc) => {
pdfDoc.getPage(1).then((page) => {
page.getTextContent().then((textContent) => {
const extractedText = mergeTextContent(textContent);
setResumeText(extractedText);
});
});
},
(reason) => {
console.error(`Error during PDF loading: ${reason}`);
}
);
}
};
reader.readAsArrayBuffer(pdfFile);
};

const handleResumeUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
setError('');
setIsLoading(true);
setResumeText('');

try {
const file = event.target.files?.[0];
if (!file) {
setError("The PDF wasn't uploaded correctly.");
setIsLoading(false);
return;
}
await readResume(file);
} catch (error) {
setError('There was an error reading the resume. Please try again.');
} finally {
setIsLoading(false);
}
};

return (
<div>
<div className={styles.fileUploadBtnContainer}>
<input
type="file"
id="file-upload"
onChange={handleResumeUpload}
accept="application/pdf"
hidden
/>
<label htmlFor="file-upload" className={`${styles.label} ${styles.mainBtn}`}>
<MdCloudUpload /> Upload resume
</label>
</div>
{error && <p className={styles.errorMessage}>{error}</p>}
</div>
);
};

export default ResumeUploader;
78 changes: 78 additions & 0 deletions app/components/ResumeWorth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import styles from '../styles/ResumeWorth.module.css';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "./ui/card";

interface ResumeWorthProps {
resumeWorth: string;
}

const ResumeWorth: React.FC<ResumeWorthProps> = ({ resumeWorth }) => {
// Extract the estimated worth, explanation, and improvements from the analysis result
const estimatedWorthMatch = resumeWorth.match(/<Estimated Worth>\$(.+?)<\/Estimated Worth>/);
const explanationMatch = resumeWorth.match(/<Explanation>([\s\S]*?)<\/Explanation>/);
const improvementsMatch = resumeWorth.match(/<Improvements>([\s\S]*?)<\/Improvements>/);

const estimatedWorth = estimatedWorthMatch ? estimatedWorthMatch[1] : 'N/A';
const explanation = explanationMatch ? explanationMatch[1] : '';
const improvements = improvementsMatch ? improvementsMatch[1] : '';

// Extract the list items from the explanation and improvements
const explanationItems = explanation.match(/<li>(.+?)<\/li>/g);
const improvementItems = improvements.match(/<li>(.+?)<\/li>/g);

return (
<div className={styles.container}>
<div className={styles.worth}>${estimatedWorth}</div>
<p className={styles.subtitle}>Resume worth</p>

<div className={styles.content}>
<div className={styles.column}>
<Card className="h-full">
<CardHeader>
<CardTitle>Key Factors</CardTitle>
<CardDescription>What contributes to your worth</CardDescription>
</CardHeader>
<CardContent>
{explanationItems && (
<ul className={styles.list}>
{explanationItems.map((item, index) => (
<li key={index} className={styles.listItem}>
{item.replace(/<\/?li>/g, '')}
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
<div className={styles.column}>
<Card className="h-full">
<CardHeader>
<CardTitle>Improvements</CardTitle>
<CardDescription>How to worth more</CardDescription>
</CardHeader>
<CardContent>
{improvementItems && (
<ul className={styles.list}>
{improvementItems.map((item, index) => (
<li key={index} className={styles.listItem}>
{item.replace(/<\/?li>/g, '')}
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
</div>
</div>
);
};

export default ResumeWorth;
Loading

0 comments on commit f9f009a

Please sign in to comment.