-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f9f009a
Showing
26 changed files
with
7,661 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.