Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated ChatGPT #5

Merged
merged 1 commit into from
Jan 21, 2024
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MONGODB_URI=mongodb+srv://&w=majority
JWT_SECRET='very-secret'
9 changes: 9 additions & 0 deletions TODO.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@
6. Integrate ChatGPT - to make resume content better
7. Duplicate resume
8. Duplicate job opening


Bugs
1. Mobile view of table jobs
2. Resume name not getting updated
3. Delete button from Job detail does not work
4. Download button from Resume Card list does not work
5. Get more keys for ChatGPT or use paid version
6. Add rate limiting in backend and move ChatGPT logic to backend
8 changes: 4 additions & 4 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/src/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY='your-api-key-here'
3 changes: 2 additions & 1 deletion client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ function App() {
</Box>
<Routes>
{/* Redirect from / and /home to /home/resume */}
<Route path="/" element={<Navigate to="/home/resume" replace />} />
<Route path="/" element={<Navigate to="/resume/new" replace />} />
{/* <Route path="/" element={<Navigate to="/home/resume" replace />} /> */}
<Route path="/home" element={<Navigate to="/home/resume" replace />} />

<Route path="/home/:tab" element={<Home />} />
Expand Down
2 changes: 1 addition & 1 deletion client/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const instance = axios.create({ baseURL });
instance.interceptors.request.use((config) => {
// using this token for dev only
const token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY1YWJiOWViYTNhOTY0Mzk2Y2FmODk1YyIsImlhdCI6MTcwNTc1Mzc4MiwiZXhwIjoxNzA1ODQwMTgyfQ.F0000STVFT2Q87LkFfp8G849JK8W84TUR3F-NysiX4Q";
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY1YWJiOWViYTNhOTY0Mzk2Y2FmODk1YyIsImlhdCI6MTcwNTg3MDM5NSwiZXhwIjoxNzA1OTU2Nzk1fQ.kn1XpSoTa9R-btlvmzIfM0CHNOhugdl0RDZjCn3S9Xo";

if (token) {
config.headers.Authorization = `Bearer ${token}`;
Expand Down
23 changes: 22 additions & 1 deletion client/src/components/FloatingDownloadButton.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import { Fab } from "@mui/material";
import { Download } from "@mui/icons-material";
import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh";
import SaveIcon from "@mui/icons-material/Save";
import Tooltip from "@mui/material/Tooltip";

export const FloatingAIButton = ({ handleClick, disabled }) => {
return (
<Tooltip title="Improve your resume with our AI" placement="top">
<Fab
disabled={disabled}
onClick={handleClick}
color="primary"
aria-label="add"
style={{
position: "fixed",
bottom: "16px",
right: "184px",
zIndex: 1000, // Adjust the z-index as needed
}}
>
<AutoFixHighIcon />
</Fab>
</Tooltip>
);
};
export const FloatingDownloadButton = ({ handleClick }) => {
return (
<Fab
Expand All @@ -19,7 +41,6 @@ export const FloatingDownloadButton = ({ handleClick }) => {
</Fab>
);
};

export const FloatingSaveButton = ({ handleClick }) => {
return (
<Fab
Expand Down
2 changes: 1 addition & 1 deletion client/src/constants/resumeBuilder.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const initialBasicDetails = {
name: "John Doe",
name: "Don Joe",
profilePicture: "https://example.com/profile-picture.jpg",
currentJobTitle: "Software Engineer",
email: "john.doe@example.com",
Expand Down
12 changes: 6 additions & 6 deletions client/src/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

const root = ReactDOM.createRoot(document.getElementById('root'));
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
Expand Down
16 changes: 14 additions & 2 deletions client/src/pages/BuildResume.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@ import jsPDF from "jspdf";
import html2canvas from "html2canvas";
import ResumeAccordion from "../components/ResumeAccordion";
import ResumePreview from "../components/ResumePreview";
import { FloatingDownloadButton, FloatingSaveButton } from "../components/FloatingDownloadButton";
import { FloatingAIButton, FloatingDownloadButton, FloatingSaveButton } from "../components/FloatingDownloadButton";
import MenuIcon from "@mui/icons-material/Menu";
import { useSelector, useDispatch } from "react-redux";
import { useParams, useNavigate } from "react-router-dom";
import { addResume, fetchResumeById, updateResume, updateResumeName } from "../reducers/resumeBuilderSlice";
import {
addResume,
fetchResumeById,
improveResumeWithGPT,
updateResume,
updateResumeName,
} from "../reducers/resumeBuilderSlice";

const ResumeBuilder = () => {
const { id: resumeId } = useParams();
const isCreateMode = resumeId === "new";
const navigate = useNavigate();
const dispatch = useDispatch();
const selectedResume = useSelector((state) => state.resumeBuilder.selectedResume);
const loading = useSelector((state) => state.resumeBuilder.loading);
const ref = useRef(null);
const isMobile = useMediaQuery("(max-width:600px)");
const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
Expand Down Expand Up @@ -67,6 +74,10 @@ const ResumeBuilder = () => {
const handleResumeNameChange = (e) => {
dispatch(updateResumeName(e.target.value));
};

const handleAIButtonClick = (e) => {
dispatch(improveResumeWithGPT(selectedResume));
};
const resumeNameInput = (
<TextField
label="Resume"
Expand Down Expand Up @@ -109,6 +120,7 @@ const ResumeBuilder = () => {
)}
</Grid>

<FloatingAIButton handleClick={handleAIButtonClick} disabled={loading} />
<FloatingSaveButton handleClick={handleSave} />
<FloatingDownloadButton handleClick={handleDownload} />
</Container>
Expand Down
22 changes: 22 additions & 0 deletions client/src/reducers/resumeBuilderSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ import { createSlice } from "@reduxjs/toolkit";
import { initialEducationDetails, initialExperience, initialResumeState } from "../constants/resumeBuilder";
import { createAsyncThunk } from "@reduxjs/toolkit";
import api from "../api";
import { improveResumeJSONWithGPT } from "../utils/openai.js";

// Async Thunks
export const fetchAllResumes = createAsyncThunk("resume/fetchAllResumes", async () => {
const response = await api.get("resumes");
return response.data;
});

export const improveResumeWithGPT = createAsyncThunk("resume/improveWithGPT", async (resume) => {
const improvedResume = await improveResumeJSONWithGPT(resume);
return improvedResume;
});

export const fetchResumeById = createAsyncThunk("resume/fetchResume", async (id) => {
console.log({ initialResumeState });
if (id === "new") return initialResumeState;
const response = await api.get(`/resumes/${id}`);

Expand Down Expand Up @@ -38,6 +46,7 @@ const resumeBuilderSlice = createSlice({
initialState: {
resumes: [],
selectedResume: { ...initialResumeState },
loading: false,
},
reducers: {
updateResumeName: (state, action) => {
Expand Down Expand Up @@ -130,6 +139,19 @@ const resumeBuilderSlice = createSlice({
})
.addCase(addResume.fulfilled, (state, action) => {
state.resumes = [...state.resumes, action.payload]; // Assuming action.payload is the entire resume object
})
.addCase(improveResumeWithGPT.pending, (state) => {
state.loading = true;
})
.addCase(improveResumeWithGPT.fulfilled, (state, action) => {
state.loading = false;
console.log("this is the payload", action.payload);
console.log("this is the update state", { ...state.selectedResume, ...action.payload });
const upadtedState = { ...state.selectedResume, ...action.payload };
state.selectedResume = upadtedState;
})
.addCase(improveResumeWithGPT.rejected, (state) => {
state.loading = false;
});
},
});
Expand Down
1 change: 1 addition & 0 deletions client/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import rootReducer from "./reducers";
const store = configureStore({
reducer: rootReducer,
// Additional middleware and configuration if needed
middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
});

export default store;
72 changes: 72 additions & 0 deletions client/src/utils/openai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import OpenAI from "openai";

console.log({ apiKey: process.env.REACT_APP_OPENAI_API_KEY });
const openai = new OpenAI({ apiKey: process.env.REACT_APP_OPENAI_API_KEY, dangerouslyAllowBrowser: true });

const improveWithGPT = async (text) => {
try {
const completion = await openai.chat.completions.create({
messages: [
{
role: "user",
content: `I am giving you an input which is part of resume of a candidate, update the text by making it more impactful. Return me a JSON with key "response".
this is the input - ${text}`,
},
],
model: "gpt-3.5-turbo-1106",
response_format: { type: "json_object" },
});

const jsonResponse = JSON.parse(completion.choices[0].message.content);
const improvedText = jsonResponse["response"];
return improvedText;
} catch (error) {
console.error("Error improving single text from GPT", error.message);
return text;
}
};

const improveContentWithGPTParallel = async (elements) => {
// Use Promise.all to make parallel API calls
const results = await Promise.all(
elements.map(async (element, index) => {
try {
// Make your asynchronous API call here
const improvedText = await improveWithGPT(element);
return { id: index, content: improvedText };
} catch (error) {
// Handle errors if needed
console.error(`Error fetching data for element ${element}: ${error}`);
return { id: index, content: null };
}
})
);

// Re-create a new array with the results in the original order
const reorderedResults = results.sort((a, b) => a.id - b.id).map((result) => result.content);
return reorderedResults;
};

export const improveResumeJSONWithGPT = async (inputResumeJSON) => {
console.log("This is the type of resume", typeof inputResumeJSON);
try {
const originalProjectDescriptions = inputResumeJSON.projectList.map((item) => item.description);
const improvedProjectDescriptions = await improveContentWithGPTParallel(originalProjectDescriptions);

const originalExperienceDescriptions = inputResumeJSON.experienceList.map((item) => item.description);
const improvedExperienceDescriptions = await improveContentWithGPTParallel(originalExperienceDescriptions);

const improvedResumeJSON = JSON.parse(JSON.stringify(inputResumeJSON));
originalProjectDescriptions.forEach(
(ele, index) => (improvedResumeJSON.projectList[index].description = improvedProjectDescriptions[index])
);
originalExperienceDescriptions.forEach(
(ele, index) => (improvedResumeJSON.experienceList[index].description = improvedExperienceDescriptions[index])
);

return improvedResumeJSON;
} catch (err) {
console.log("Error while improving with GPT", err);
return inputResumeJSON;
}
};
Loading